From a23c4d4159b064dee3faea82696ebfcf6b71b847 Mon Sep 17 00:00:00 2001 From: dagger Date: Sat, 22 Aug 2026 10:50:08 +0700 Subject: [PATCH 01/56] add crypto library --- .github/workflows/appimage.yml | 4 +- .github/workflows/cmake.yml | 2 +- Dockerfile | 1 + doc/en/scripting.md | 3 +- doc/en/scripting/builtins/libcrypto.md | 140 +++ doc/ru/scripting.md | 1 + doc/ru/scripting/builtins/libcrypto.md | 159 +++ flake.nix | 1 + src/CMakeLists.txt | 6 +- src/crypto/Crypto.cpp | 1257 ++++++++++++++++++++ src/crypto/Crypto.hpp | 185 +++ src/logic/scripting/lua/libs/api_lua.hpp | 2 + src/logic/scripting/lua/libs/libcrypto.cpp | 540 +++++++++ src/logic/scripting/lua/lua_engine.cpp | 2 + test/crypto/Crypto.cpp | 470 ++++++++ test/crypto/CryptoLua.cpp | 70 ++ vcpkg.json | 3 +- 17 files changed, 2839 insertions(+), 7 deletions(-) create mode 100644 doc/en/scripting/builtins/libcrypto.md create mode 100644 doc/ru/scripting/builtins/libcrypto.md create mode 100644 src/crypto/Crypto.cpp create mode 100644 src/crypto/Crypto.hpp create mode 100644 src/logic/scripting/lua/libs/libcrypto.cpp create mode 100644 test/crypto/Crypto.cpp create mode 100644 test/crypto/CryptoLua.cpp diff --git a/.github/workflows/appimage.yml b/.github/workflows/appimage.yml index f43254bd3..8df68ff02 100644 --- a/.github/workflows/appimage.yml +++ b/.github/workflows/appimage.yml @@ -31,7 +31,7 @@ jobs: sudo apt-get update sudo apt-get install -y build-essential libglfw3-dev libglfw3 libglew-dev libglew2.2 \ libglm-dev libpng-dev libopenal-dev libluajit-5.1-dev libvorbis-dev \ - libcurl4-openssl-dev libgtest-dev libfreetype6-dev cmake squashfs-tools valgrind + libcurl4-openssl-dev libssl-dev libgtest-dev libfreetype6-dev cmake squashfs-tools valgrind # fix luajit paths sudo ln -s /usr/lib/x86_64-linux-gnu/libluajit-5.1.a /usr/lib/x86_64-linux-gnu/liblua5.1.a sudo ln -s /usr/include/luajit-2.1 /usr/include/lua @@ -46,7 +46,7 @@ jobs: - name: Build run: cmake --build build -t install - name: Run tests - run: ctest --test-dir build + run: ctest --output-on-failure --test-dir build - name: Run engine tests timeout-minutes: 1 run: | diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index f41e940e9..52f7b7183 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -39,7 +39,7 @@ jobs: # make && make install INSTALL_INC=/usr/include/lua run: | sudo apt-get update - sudo apt-get install libglfw3-dev libglfw3 libglew-dev libglm-dev libpng-dev libopenal-dev libluajit-5.1-dev libvorbis-dev libgtest-dev libcurl4-openssl-dev libfreetype6-dev + sudo apt-get install libglfw3-dev libglfw3 libglew-dev libglm-dev libpng-dev libopenal-dev libluajit-5.1-dev libvorbis-dev libgtest-dev libcurl4-openssl-dev libssl-dev libfreetype6-dev # fix luajit paths sudo ln -s /usr/lib/x86_64-linux-gnu/libluajit-5.1.a /usr/lib/x86_64-linux-gnu/liblua-5.1.a sudo ln -s /usr/include/luajit-2.1 /usr/include/lua diff --git a/Dockerfile b/Dockerfile index da90324fc..3a06d1afc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,6 +26,7 @@ RUN apt-get update && apt-get install --no-install-recommends -y \ libvorbis-dev \ libcurl4-openssl-dev \ libfreetype6-dev \ + libssl-dev \ ca-certificates \ wget \ && rm -rf /var/lib/apt/lists/* diff --git a/doc/en/scripting.md b/doc/en/scripting.md index 2072d4353..5557c8185 100644 --- a/doc/en/scripting.md +++ b/doc/en/scripting.md @@ -14,7 +14,8 @@ Subsections: - [base64](scripting/builtins/libbase64.md) - [bjson, json, toml, yaml](scripting/filesystem.md) - [block](scripting/builtins/libblock.md) - - [byteutil](scripting/builtins/libbyteutil.md) + - [byteutil](scripting/builtins/libbyteutil.md) + - [crypto](scripting/builtins/libcrypto.md) - [cameras](scripting/builtins/libcameras.md) - [ctypes](scripting/builtins/libctypes.md) - [entities](scripting/builtins/libentities.md) diff --git a/doc/en/scripting/builtins/libcrypto.md b/doc/en/scripting/builtins/libcrypto.md new file mode 100644 index 000000000..19b93e433 --- /dev/null +++ b/doc/en/scripting/builtins/libcrypto.md @@ -0,0 +1,140 @@ +# *crypto* library + +The library provides common cryptographic functions for mods + +TLS and HTTP are not part of this library. They can use crypto for expensive +operations + +Keys, signatures, hashes and encrypted data are passed as regular Lua strings. +Strings may contain zero bytes + +The current API version is available in `crypto.API_VERSION` + +## Hashing + +```lua +crypto.sha256(data: str) -> str +crypto.sha384(data: str) -> str +crypto.sha512(data: str) -> str +crypto.md5(data: str) -> str +crypto.hash(hash: str, data: str) -> str +crypto.hmac(hash: str, key: str, data: str) -> str +``` + +Supported names are `SHA256`, `SHA384`, `SHA512` and `MD5` + +MD5 is provided for old formats. SHA256 should be used for new data + +Large data can be hashed in parts + +```lua +local hash = crypto.hash_new("SHA256") +hash:update(part1) +hash:update(part2) +local result = hash:final() +``` + +Call `reset` before using the context again + +## Signatures + +```lua +crypto.ed25519_keypair() -> private_key, public_key +crypto.ed25519_public(private_key: str) -> str +crypto.ed25519_sign(private_key: str, message: str) -> str +crypto.ed25519_verify(public_key: str, message: str, signature: str) + -> true + -> false, error + +crypto.ecdsa_keypair(curve: str) -> private_key, public_key +crypto.ecdsa_public(curve: str, private_key: str) -> str +crypto.ecdsa_sign(curve: str, private_key: str, message: str, hash: str) -> str +crypto.ecdsa_verify(curve: str, public_key: str, message: str, + signature: str, hash: str) + -> true + -> false, error + +crypto.rsa_pkcs1_verify(hash: str, modulus: str, exponent: str, + message: str, signature: str) + -> true + -> false, error +crypto.rsa_pss_verify(hash: str, modulus: str, exponent: str, + message: str, signature: str, salt_length: int) + -> true + -> false, error +``` + +Ed25519 keys are 32 bytes and signatures are 64 bytes + +ECDSA supports `P-256`, `P-384` and `P-521`. Public keys use the uncompressed +SEC1 format and signatures use DER + +RSA modulus and exponent use big endian. A `salt_length` of `-1` uses the hash +size. RSA key generation is not provided, Ed25519 is easier for new keys + +## Key exchange + +```lua +crypto.x25519_keypair() -> private_key, public_key +crypto.x25519_public(private_key: str) -> str +crypto.x25519(private_key: str, peer_public_key: str) -> str +crypto.x25519_shared(private_key: str, peer_public_key: str) -> str + +crypto.p256_keypair() -> private_key, public_key +crypto.p256_public(private_key: str) -> str +crypto.p256_shared(private_key: str, peer_public_key: str) -> str +``` + +`x25519` and `x25519_shared` do the same operation + +Pass the shared secret through HKDF before using it as a key + +## Encryption + +```lua +crypto.aes_gcm_encrypt(key: str, nonce: str, aad: str, plaintext: str) -> str +crypto.aes_gcm_decrypt(key: str, nonce: str, aad: str, ciphertext: str) + -> plaintext + -> nil, error + +crypto.chacha20_poly1305_encrypt(key: str, nonce: str, aad: str, + plaintext: str) -> str +crypto.chacha20_poly1305_decrypt(key: str, nonce: str, aad: str, + ciphertext: str) + -> plaintext + -> nil, error +``` + +AES GCM accepts 16 and 32 byte keys + +ChaCha20 Poly1305 uses a 32 byte key and a 12 byte nonce + +A 16 byte tag is appended to ciphertext. Decryption returns nothing when the +tag is invalid + +Never use the same nonce twice with the same key + +## Other functions + +```lua +crypto.random_bytes(length: int) -> str +crypto.constant_time_equal(left: str, right: str) -> bool +crypto.hkdf_extract(hash: str, salt: str, ikm: str) -> str +crypto.hkdf_expand(hash: str, prk: str, info: str, length: int) -> str +crypto.pbkdf2(hash: str, password: str, salt: str, + iterations: int, length: int) -> str +crypto.scrypt(password: str, salt: str, n: int, r: int, p: int, + length: int, [optional]max_memory: int=0) -> str +crypto.features() -> table +``` + +Use scrypt or PBKDF2 with a random salt for passwords. A regular SHA256 hash is +not suitable for password storage + +`features` returns the OpenSSL version and available functions + +An invalid signature returns `false, "invalid_signature"` + +An invalid encryption tag returns `nil, "authentication_failed"` + +Invalid arguments raise a regular Lua error diff --git a/doc/ru/scripting.md b/doc/ru/scripting.md index 1e6d73098..dde26f0b9 100644 --- a/doc/ru/scripting.md +++ b/doc/ru/scripting.md @@ -15,6 +15,7 @@ - [bjson, json, toml, yaml](scripting/filesystem.md) - [block](scripting/builtins/libblock.md) - [byteutil](scripting/builtins/libbyteutil.md) + - [crypto](scripting/builtins/libcrypto.md) - [cameras](scripting/builtins/libcameras.md) - [ctypes](scripting/builtins/libctypes.md) - [entities](scripting/builtins/libentities.md) diff --git a/doc/ru/scripting/builtins/libcrypto.md b/doc/ru/scripting/builtins/libcrypto.md new file mode 100644 index 000000000..dce86d401 --- /dev/null +++ b/doc/ru/scripting/builtins/libcrypto.md @@ -0,0 +1,159 @@ +# Библиотека *crypto* + +Библиотека предоставляет основные криптографические функции для модов + +TLS и HTTP в нее не входят. Эти протоколы используют crypto для тяжелых +вычислений + +Все бинарные значения передаются обычными Lua строками. Это касается ключей, +подписей, хешей и зашифрованных данных. Строки могут содержать нулевые байты + +Текущая версия API находится в `crypto.API_VERSION` + +## Хеширование + +```lua +crypto.sha256(data: str) -> str +crypto.sha384(data: str) -> str +crypto.sha512(data: str) -> str +crypto.md5(data: str) -> str + +crypto.hash(hash: str, data: str) -> str +crypto.hmac(hash: str, key: str, data: str) -> str +``` + +Поддерживаются `SHA256`, `SHA384`, `SHA512` и `MD5` + +MD5 нужен для совместимости со старыми форматами. Для новых данных лучше +использовать SHA256 + +Большой файл можно передавать частями + +```lua +local hash = crypto.hash_new("SHA256") + +hash:update(part1) +hash:update(part2) + +local result = hash:final() +``` + +После `final` контекст можно очистить через `reset` + +## Подписи + +```lua +crypto.ed25519_keypair() -> private_key, public_key +crypto.ed25519_public(private_key: str) -> str +crypto.ed25519_sign(private_key: str, message: str) -> str +crypto.ed25519_verify(public_key: str, message: str, signature: str) + -> true + -> false, error +``` + +Ключи Ed25519 имеют длину 32 байта. Подпись имеет длину 64 байта + +```lua +crypto.ecdsa_keypair(curve: str) -> private_key, public_key +crypto.ecdsa_public(curve: str, private_key: str) -> str +crypto.ecdsa_sign(curve: str, private_key: str, message: str, hash: str) -> str +crypto.ecdsa_verify(curve: str, public_key: str, message: str, + signature: str, hash: str) + -> true + -> false, error +``` + +Доступны кривые `P-256`, `P-384` и `P-521` + +Публичный ключ ECDSA записывается как несжатая SEC1 точка. Подпись хранится в +DER формате + +```lua +crypto.rsa_pkcs1_verify(hash: str, modulus: str, exponent: str, + message: str, signature: str) + -> true + -> false, error + +crypto.rsa_pss_verify(hash: str, modulus: str, exponent: str, + message: str, signature: str, salt_length: int) + -> true + -> false, error +``` + +Модуль и экспонента RSA передаются в big endian. Значение `salt_length = -1` +использует размер хеша + +Генерации RSA ключей в библиотеке нет. Для новых ключей проще использовать +Ed25519 + +## Обмен ключами + +```lua +crypto.x25519_keypair() -> private_key, public_key +crypto.x25519_public(private_key: str) -> str +crypto.x25519(private_key: str, peer_public_key: str) -> str +crypto.x25519_shared(private_key: str, peer_public_key: str) -> str + +crypto.p256_keypair() -> private_key, public_key +crypto.p256_public(private_key: str) -> str +crypto.p256_shared(private_key: str, peer_public_key: str) -> str +``` + +`x25519` и `x25519_shared` выполняют одну и ту же операцию + +Общий секрет не стоит использовать как готовый ключ. Сначала его нужно +обработать через HKDF + +## Шифрование + +```lua +crypto.aes_gcm_encrypt(key: str, nonce: str, aad: str, plaintext: str) -> str +crypto.aes_gcm_decrypt(key: str, nonce: str, aad: str, ciphertext: str) + -> plaintext + -> nil, error + +crypto.chacha20_poly1305_encrypt(key: str, nonce: str, aad: str, + plaintext: str) -> str +crypto.chacha20_poly1305_decrypt(key: str, nonce: str, aad: str, + ciphertext: str) + -> plaintext + -> nil, error +``` + +AES GCM принимает ключи размером 16 или 32 байта + +ChaCha20 Poly1305 использует ключ размером 32 байта и nonce размером 12 байт + +В конец зашифрованных данных добавляется тег размером 16 байт. При неверном +теге функция расшифровки ничего не возвращает + +Нельзя повторно использовать один nonce с тем же ключом + +## Остальные функции + +```lua +crypto.random_bytes(length: int) -> str +crypto.constant_time_equal(left: str, right: str) -> bool + +crypto.hkdf_extract(hash: str, salt: str, ikm: str) -> str +crypto.hkdf_expand(hash: str, prk: str, info: str, length: int) -> str + +crypto.pbkdf2(hash: str, password: str, salt: str, + iterations: int, length: int) -> str +crypto.scrypt(password: str, salt: str, n: int, r: int, p: int, + length: int, [опционально]max_memory: int=0) -> str + +crypto.features() -> table +``` + +Для хранения паролей следует использовать scrypt или PBKDF2 со случайной +солью. Обычный SHA256 для паролей не подходит + +`features` возвращает версию OpenSSL и список доступных возможностей. Это +можно использовать если мод запускается на разных сборках движка + +Неверная подпись возвращает `false, "invalid_signature"` + +Неверный тег при расшифровке возвращает `nil, "authentication_failed"` + +Ошибки в аргументах вызывают обычную Lua ошибку diff --git a/flake.nix b/flake.nix index ee1e6baa9..ead09d092 100644 --- a/flake.nix +++ b/flake.nix @@ -37,6 +37,7 @@ openal luajit curl + openssl entt mesa freeglut diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 12ece0f7f..bbe8a9763 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -18,6 +18,7 @@ endif() find_package(ZLIB REQUIRED) find_package(PNG REQUIRED) find_package(CURL REQUIRED) +find_package(OpenSSL 3.0 REQUIRED COMPONENTS Crypto) find_package(glfw3 REQUIRED) find_package(Freetype REQUIRED) if(NOT APPLE) @@ -79,12 +80,13 @@ target_link_libraries( ZLIB::ZLIB PNG::PNG CURL::libcurl + OpenSSL::Crypto OpenAL::OpenAL Vorbis::vorbis Vorbis::vorbisfile - luajit::luajit Freetype::Freetype - PUBLIC glm::glm # Need public for src/delegates.hpp, which including to + PUBLIC luajit::luajit + glm::glm # Need public for src/delegates.hpp, which including to # main.cpp ) diff --git a/src/crypto/Crypto.cpp b/src/crypto/Crypto.cpp new file mode 100644 index 000000000..169f94d65 --- /dev/null +++ b/src/crypto/Crypto.cpp @@ -0,0 +1,1257 @@ +#include "Crypto.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace crypto { + Error::Error(ErrorCode code, const std::string& message) + : std::runtime_error(message), errorCode(code) { + } + + ErrorCode Error::code() const noexcept { + return errorCode; + } + + const char* Error::codeName() const noexcept { + switch (errorCode) { + case ErrorCode::InvalidArgument: + return "invalid_argument"; + case ErrorCode::InvalidKey: + return "invalid_key"; + case ErrorCode::InvalidSignature: + return "invalid_signature"; + case ErrorCode::AuthenticationFailed: + return "authentication_failed"; + case ErrorCode::UnsupportedAlgorithm: + return "unsupported_algorithm"; + case ErrorCode::OutputTooLarge: + return "output_too_large"; + case ErrorCode::InvalidState: + return "invalid_state"; + case ErrorCode::BackendError: + return "backend_error"; + } + return "backend_error"; + } + + namespace { + constexpr std::size_t AEAD_TAG_SIZE = 16; + + template + using Handle = std::unique_ptr; + + using BnHandle = Handle; + using EcGroupHandle = Handle; + using EcPointHandle = Handle; + using EvpCipherCtxHandle = Handle; + using EvpMdCtxHandle = Handle; + using EvpPkeyHandle = Handle; + using EvpPkeyCtxHandle = Handle; + using ParamBldHandle = Handle; + using ParamHandle = Handle; + + const unsigned char* bytes(std::string_view value) { + return reinterpret_cast(value.data()); + } + + std::string opensslError(const std::string& operation) { + const unsigned long code = ERR_get_error(); + if (code == 0) { + return operation; + } + char message[256]; + ERR_error_string_n(code, message, sizeof(message)); + return operation + ": " + message; + } + + [[noreturn]] void fail(const std::string& operation) { + throw Error(ErrorCode::BackendError, opensslError(operation)); + } + + int checkedSize(std::size_t size, const char* name) { + if (size > static_cast(INT_MAX)) { + throw Error( + ErrorCode::OutputTooLarge, + std::string(name) + " is too large" + ); + } + return static_cast(size); + } + + const EVP_MD* getShaDigest(std::string_view hash) { + if (hash == "SHA256") return EVP_sha256(); + if (hash == "SHA384") return EVP_sha384(); + if (hash == "SHA512") return EVP_sha512(); + throw Error( + ErrorCode::UnsupportedAlgorithm, + "unsupported hash: " + std::string(hash) + ); + } + + const EVP_MD* getDigest(std::string_view hash) { + if (hash == "MD5") return EVP_md5(); + return getShaDigest(hash); + } + + const char* curveName(std::string_view curve) { + if (curve == "P-256") return "prime256v1"; + if (curve == "P-384") return "secp384r1"; + if (curve == "P-521") return "secp521r1"; + throw Error( + ErrorCode::UnsupportedAlgorithm, + "unsupported curve: " + std::string(curve) + ); + } + + int curveNid(std::string_view curve) { + if (curve == "P-256") return NID_X9_62_prime256v1; + if (curve == "P-384") return NID_secp384r1; + if (curve == "P-521") return NID_secp521r1; + curveName(curve); + return NID_undef; + } + + std::size_t curvePrivateSize(std::string_view curve) { + if (curve == "P-256") return 32; + if (curve == "P-384") return 48; + if (curve == "P-521") return 66; + curveName(curve); + return 0; + } + + EvpPkeyHandle fromData( + const char* algorithm, + int selection, + OSSL_PARAM_BLD* builder, + ErrorCode failureCode = ErrorCode::BackendError + ) { + ParamHandle params( + OSSL_PARAM_BLD_to_param(builder), OSSL_PARAM_free + ); + EvpPkeyCtxHandle ctx( + EVP_PKEY_CTX_new_from_name(nullptr, algorithm, nullptr), + EVP_PKEY_CTX_free + ); + EVP_PKEY* rawKey = nullptr; + if (!params || !ctx || EVP_PKEY_fromdata_init(ctx.get()) <= 0 || + EVP_PKEY_fromdata( + ctx.get(), &rawKey, selection, params.get() + ) <= 0) { + throw Error( + failureCode, + opensslError( + std::string("failed to import ") + algorithm + " key" + ) + ); + } + return EvpPkeyHandle(rawKey, EVP_PKEY_free); + } + + void checkPublicKey(EVP_PKEY* key, const char* name) { + EvpPkeyCtxHandle ctx( + EVP_PKEY_CTX_new(key, nullptr), EVP_PKEY_CTX_free + ); + if (!ctx || EVP_PKEY_public_check(ctx.get()) != 1) { + throw Error( + ErrorCode::InvalidKey, + opensslError(std::string("invalid ") + name + " public key") + ); + } + } + + EvpPkeyHandle makeEcdsaPublicKey( + std::string_view curve, std::string_view encoded + ) { + if (encoded.empty() || + static_cast(encoded[0]) != 0x04) { + throw Error( + ErrorCode::InvalidKey, + "ECDSA public key must be an uncompressed SEC1 point" + ); + } + ParamBldHandle builder(OSSL_PARAM_BLD_new(), OSSL_PARAM_BLD_free); + const char* group = curveName(curve); + if (!builder || + OSSL_PARAM_BLD_push_utf8_string( + builder.get(), OSSL_PKEY_PARAM_GROUP_NAME, group, 0 + ) != 1 || + OSSL_PARAM_BLD_push_octet_string( + builder.get(), + OSSL_PKEY_PARAM_PUB_KEY, + encoded.data(), + encoded.size() + ) != 1) { + fail("failed to prepare ECDSA public key"); + } + auto result = fromData( + "EC", EVP_PKEY_PUBLIC_KEY, builder.get(), ErrorCode::InvalidKey + ); + checkPublicKey(result.get(), "ECDSA"); + return result; + } + + EvpPkeyHandle makeRsaPublicKey( + std::string_view modulus, std::string_view exponent + ) { + if (modulus.empty() || exponent.empty()) { + throw Error( + ErrorCode::InvalidKey, + "RSA modulus and exponent must not be empty" + ); + } + BnHandle n( + BN_bin2bn( + bytes(modulus), + checkedSize(modulus.size(), "modulus"), + nullptr + ), + BN_free + ); + BnHandle e( + BN_bin2bn( + bytes(exponent), + checkedSize(exponent.size(), "exponent"), + nullptr + ), + BN_free + ); + ParamBldHandle builder(OSSL_PARAM_BLD_new(), OSSL_PARAM_BLD_free); + if (!n || !e || !builder) { + fail("failed to prepare RSA public key"); + } + if (BN_is_zero(n.get()) || BN_is_zero(e.get())) { + throw Error(ErrorCode::InvalidKey, "invalid RSA public key"); + } + if (OSSL_PARAM_BLD_push_BN( + builder.get(), OSSL_PKEY_PARAM_RSA_N, n.get() + ) != 1 || + OSSL_PARAM_BLD_push_BN( + builder.get(), OSSL_PKEY_PARAM_RSA_E, e.get() + ) != 1) { + fail("invalid RSA public key"); + } + auto result = fromData( + "RSA", EVP_PKEY_PUBLIC_KEY, builder.get(), ErrorCode::InvalidKey + ); + checkPublicKey(result.get(), "RSA"); + return result; + } + + bool verifyDigestSignature( + EVP_PKEY* key, + const EVP_MD* md, + std::string_view message, + std::string_view signature, + int rsaPadding, + int saltLength + ) { + EvpMdCtxHandle ctx(EVP_MD_CTX_new(), EVP_MD_CTX_free); + EVP_PKEY_CTX* pkeyCtx = nullptr; + if (!ctx || + EVP_DigestVerifyInit(ctx.get(), &pkeyCtx, md, nullptr, key) != + 1) { + fail("signature verification initialization failed"); + } + if (rsaPadding != 0) { + if (EVP_PKEY_CTX_set_rsa_padding(pkeyCtx, rsaPadding) <= 0) { + fail("failed to configure RSA padding"); + } + if (rsaPadding == RSA_PKCS1_PSS_PADDING && + EVP_PKEY_CTX_set_rsa_pss_saltlen(pkeyCtx, saltLength) <= + 0) { + fail("failed to configure RSA-PSS salt length"); + } + } + if (EVP_DigestVerifyUpdate( + ctx.get(), message.data(), message.size() + ) != 1) { + fail("signature verification update failed"); + } + const int result = EVP_DigestVerifyFinal( + ctx.get(), bytes(signature), signature.size() + ); + if (result < 0) { + throw Error( + ErrorCode::InvalidSignature, + opensslError("invalid signature encoding") + ); + } + if (result == 0) ERR_clear_error(); + return result == 1; + } + + EvpPkeyHandle rawPrivateKey( + int type, std::string_view key, const char* name + ) { + if (key.size() != 32) { + throw Error( + ErrorCode::InvalidKey, + std::string(name) + " private key must be 32 bytes" + ); + } + EvpPkeyHandle result( + EVP_PKEY_new_raw_private_key( + type, nullptr, bytes(key), key.size() + ), + EVP_PKEY_free + ); + if (!result) fail(std::string("invalid ") + name + " private key"); + return result; + } + + EvpPkeyHandle rawPublicKey( + int type, std::string_view key, const char* name + ) { + if (key.size() != 32) { + throw Error( + ErrorCode::InvalidKey, + std::string(name) + " public key must be 32 bytes" + ); + } + EvpPkeyHandle result( + EVP_PKEY_new_raw_public_key( + type, nullptr, bytes(key), key.size() + ), + EVP_PKEY_free + ); + if (!result) fail(std::string("invalid ") + name + " public key"); + return result; + } + + const EVP_CIPHER* aesGcmCipher(std::size_t keySize) { + if (keySize == 16) return EVP_aes_128_gcm(); + if (keySize == 32) return EVP_aes_256_gcm(); + throw Error( + ErrorCode::InvalidKey, "AES-GCM key must be 16 or 32 bytes" + ); + } + + Bytes aeadEncrypt( + const EVP_CIPHER* cipher, + std::string_view key, + std::string_view nonce, + std::string_view aad, + std::string_view plaintext + ) { + if (nonce.empty()) { + throw Error( + ErrorCode::InvalidArgument, "nonce must not be empty" + ); + } + const int nonceSize = checkedSize(nonce.size(), "nonce"); + const int aadSize = checkedSize(aad.size(), "aad"); + const int plaintextSize = + checkedSize(plaintext.size(), "plaintext"); + EvpCipherCtxHandle ctx(EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free); + if (!ctx || + EVP_EncryptInit_ex( + ctx.get(), cipher, nullptr, nullptr, nullptr + ) != 1 || + EVP_CIPHER_CTX_ctrl( + ctx.get(), EVP_CTRL_AEAD_SET_IVLEN, nonceSize, nullptr + ) != 1 || + EVP_EncryptInit_ex( + ctx.get(), nullptr, nullptr, bytes(key), bytes(nonce) + ) != 1) { + fail("AEAD encryption initialization failed"); + } + int written = 0; + if (!aad.empty() && + EVP_EncryptUpdate( + ctx.get(), nullptr, &written, bytes(aad), aadSize + ) != 1) { + fail("AEAD AAD processing failed"); + } + Bytes output(plaintext.size() + AEAD_TAG_SIZE); + int outputSize = 0; + if (!plaintext.empty() && EVP_EncryptUpdate( + ctx.get(), + output.data(), + &outputSize, + bytes(plaintext), + plaintextSize + ) != 1) { + fail("AEAD encryption failed"); + } + if (EVP_EncryptFinal_ex( + ctx.get(), output.data() + outputSize, &written + ) != 1) { + fail("AEAD encryption finalization failed"); + } + outputSize += written; + if (EVP_CIPHER_CTX_ctrl( + ctx.get(), + EVP_CTRL_AEAD_GET_TAG, + AEAD_TAG_SIZE, + output.data() + outputSize + ) != 1) { + fail("AEAD tag retrieval failed"); + } + output.resize(static_cast(outputSize) + AEAD_TAG_SIZE); + return output; + } + + Bytes aeadDecrypt( + const EVP_CIPHER* cipher, + std::string_view key, + std::string_view nonce, + std::string_view aad, + std::string_view ciphertextWithTag + ) { + if (nonce.empty()) { + throw Error( + ErrorCode::InvalidArgument, "nonce must not be empty" + ); + } + if (ciphertextWithTag.size() < AEAD_TAG_SIZE) { + throw Error( + ErrorCode::AuthenticationFailed, + "ciphertext is shorter than the authentication tag" + ); + } + const std::size_t ciphertextSize = + ciphertextWithTag.size() - AEAD_TAG_SIZE; + const int nonceSize = checkedSize(nonce.size(), "nonce"); + const int aadSize = checkedSize(aad.size(), "aad"); + const int inputSize = checkedSize(ciphertextSize, "ciphertext"); + EvpCipherCtxHandle ctx(EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free); + if (!ctx || + EVP_DecryptInit_ex( + ctx.get(), cipher, nullptr, nullptr, nullptr + ) != 1 || + EVP_CIPHER_CTX_ctrl( + ctx.get(), EVP_CTRL_AEAD_SET_IVLEN, nonceSize, nullptr + ) != 1 || + EVP_DecryptInit_ex( + ctx.get(), nullptr, nullptr, bytes(key), bytes(nonce) + ) != 1) { + fail("AEAD decryption initialization failed"); + } + int written = 0; + if (!aad.empty() && + EVP_DecryptUpdate( + ctx.get(), nullptr, &written, bytes(aad), aadSize + ) != 1) { + fail("AEAD AAD processing failed"); + } + Bytes output(ciphertextSize + AEAD_TAG_SIZE); + int outputSize = 0; + if (ciphertextSize != 0 && EVP_DecryptUpdate( + ctx.get(), + output.data(), + &outputSize, + bytes(ciphertextWithTag), + inputSize + ) != 1) { + fail("AEAD decryption failed"); + } + if (EVP_CIPHER_CTX_ctrl( + ctx.get(), + EVP_CTRL_AEAD_SET_TAG, + AEAD_TAG_SIZE, + const_cast( + bytes(ciphertextWithTag) + ciphertextSize + ) + ) != 1) { + fail("AEAD tag setup failed"); + } + const int result = EVP_DecryptFinal_ex( + ctx.get(), output.data() + outputSize, &written + ); + if (result != 1) { + ERR_clear_error(); + throw Error( + ErrorCode::AuthenticationFailed, "authentication failed" + ); + } + outputSize += written; + output.resize(static_cast(outputSize)); + return output; + } + + struct EcKey { + EvpPkeyHandle key; + Bytes publicKey; + }; + + EcKey ecPrivateKey( + std::string_view curve, std::string_view privateKey + ) { + const std::size_t expectedSize = curvePrivateSize(curve); + if (privateKey.size() != expectedSize) { + throw Error( + ErrorCode::InvalidKey, + std::string(curve) + " private key must be " + + std::to_string(expectedSize) + " bytes" + ); + } + EcGroupHandle group( + EC_GROUP_new_by_curve_name(curveNid(curve)), EC_GROUP_free + ); + BnHandle scalar( + BN_bin2bn( + bytes(privateKey), + checkedSize(privateKey.size(), "private key"), + nullptr + ), + BN_free + ); + BnHandle order(BN_new(), BN_free); + if (!group || !scalar || !order || BN_is_zero(scalar.get()) || + EC_GROUP_get_order(group.get(), order.get(), nullptr) != 1 || + BN_cmp(scalar.get(), order.get()) >= 0) { + throw Error(ErrorCode::InvalidKey, "invalid EC private key"); + } + EcPointHandle publicPoint(EC_POINT_new(group.get()), EC_POINT_free); + if (!publicPoint || EC_POINT_mul( + group.get(), + publicPoint.get(), + scalar.get(), + nullptr, + nullptr, + nullptr + ) != 1) { + fail("EC public key derivation failed"); + } + const std::size_t publicSize = EC_POINT_point2oct( + group.get(), + publicPoint.get(), + POINT_CONVERSION_UNCOMPRESSED, + nullptr, + 0, + nullptr + ); + Bytes publicKey(publicSize); + if (publicSize == 0 || EC_POINT_point2oct( + group.get(), + publicPoint.get(), + POINT_CONVERSION_UNCOMPRESSED, + publicKey.data(), + publicKey.size(), + nullptr + ) != publicSize) { + fail("EC public key encoding failed"); + } + ParamBldHandle builder(OSSL_PARAM_BLD_new(), OSSL_PARAM_BLD_free); + if (!builder || + OSSL_PARAM_BLD_push_utf8_string( + builder.get(), + OSSL_PKEY_PARAM_GROUP_NAME, + curveName(curve), + 0 + ) != 1 || + OSSL_PARAM_BLD_push_BN( + builder.get(), OSSL_PKEY_PARAM_PRIV_KEY, scalar.get() + ) != 1 || + OSSL_PARAM_BLD_push_octet_string( + builder.get(), + OSSL_PKEY_PARAM_PUB_KEY, + publicKey.data(), + publicKey.size() + ) != 1) { + fail("failed to prepare EC private key"); + } + return { + fromData("EC", EVP_PKEY_KEYPAIR, builder.get()), + std::move(publicKey) + }; + } + + KeyPair rawKeyPair(const char* algorithm) { + EvpPkeyHandle key( + EVP_PKEY_Q_keygen(nullptr, nullptr, algorithm), EVP_PKEY_free + ); + if (!key) fail(std::string(algorithm) + " key generation failed"); + KeyPair result {Bytes(32), Bytes(32)}; + std::size_t privateSize = result.privateKey.size(); + std::size_t publicSize = result.publicKey.size(); + if (EVP_PKEY_get_raw_private_key( + key.get(), result.privateKey.data(), &privateSize + ) != 1 || + EVP_PKEY_get_raw_public_key( + key.get(), result.publicKey.data(), &publicSize + ) != 1) { + fail(std::string(algorithm) + " key export failed"); + } + result.privateKey.resize(privateSize); + result.publicKey.resize(publicSize); + return result; + } + + Bytes rawPublicFromPrivate( + int type, std::string_view privateKey, const char* name + ) { + auto key = rawPrivateKey(type, privateKey, name); + Bytes result(32); + std::size_t size = result.size(); + if (EVP_PKEY_get_raw_public_key(key.get(), result.data(), &size) != + 1) { + fail(std::string(name) + " public key derivation failed"); + } + result.resize(size); + return result; + } + + Bytes signDigest( + EVP_PKEY* key, const EVP_MD* md, std::string_view message + ) { + EvpMdCtxHandle ctx(EVP_MD_CTX_new(), EVP_MD_CTX_free); + if (!ctx || + EVP_DigestSignInit(ctx.get(), nullptr, md, nullptr, key) != 1 || + EVP_DigestSignUpdate( + ctx.get(), message.data(), message.size() + ) != 1) { + fail("signature initialization failed"); + } + std::size_t size = 0; + if (EVP_DigestSignFinal(ctx.get(), nullptr, &size) != 1) { + fail("signature size query failed"); + } + Bytes signature(size); + if (EVP_DigestSignFinal(ctx.get(), signature.data(), &size) != 1) { + fail("signing failed"); + } + signature.resize(size); + return signature; + } + } + + struct HashContext::Impl { + explicit Impl(std::string_view name) + : digest(getDigest(name)), ctx(EVP_MD_CTX_new(), EVP_MD_CTX_free) { + if (!ctx || EVP_DigestInit_ex(ctx.get(), digest, nullptr) != 1) { + fail("hash context initialization failed"); + } + } + + const EVP_MD* digest; + EvpMdCtxHandle ctx; + bool finalized = false; + }; + + HashContext::HashContext(std::string_view hash) + : impl(std::make_unique(hash)) { + } + + HashContext::~HashContext() = default; + HashContext::HashContext(HashContext&&) noexcept = default; + HashContext& HashContext::operator=(HashContext&&) noexcept = default; + + void HashContext::update(std::string_view data) { + if (impl->finalized) { + throw Error(ErrorCode::InvalidState, "hash context is finalized"); + } + if (EVP_DigestUpdate(impl->ctx.get(), data.data(), data.size()) != 1) { + fail("hash update failed"); + } + } + + Bytes HashContext::final() { + if (impl->finalized) { + throw Error(ErrorCode::InvalidState, "hash context is finalized"); + } + Bytes output(static_cast(EVP_MD_size(impl->digest))); + unsigned int size = 0; + if (EVP_DigestFinal_ex(impl->ctx.get(), output.data(), &size) != 1) { + fail("hash finalization failed"); + } + impl->finalized = true; + output.resize(size); + return output; + } + + void HashContext::reset() { + if (EVP_DigestInit_ex(impl->ctx.get(), impl->digest, nullptr) != 1) { + fail("hash reset failed"); + } + impl->finalized = false; + } + + Bytes digest(std::string_view hash, std::string_view data) { + const EVP_MD* md = getDigest(hash); + Bytes output(static_cast(EVP_MD_size(md))); + unsigned int size = 0; + if (EVP_Digest( + data.data(), data.size(), output.data(), &size, md, nullptr + ) != 1) { + fail("digest calculation failed"); + } + output.resize(size); + return output; + } + + Bytes hmac( + std::string_view hash, std::string_view key, std::string_view data + ) { + const EVP_MD* md = getShaDigest(hash); + Bytes output(static_cast(EVP_MD_size(md))); + unsigned int size = 0; + if (!HMAC( + md, + key.data(), + checkedSize(key.size(), "HMAC key"), + bytes(data), + data.size(), + output.data(), + &size + )) { + fail("HMAC calculation failed"); + } + output.resize(size); + return output; + } + + KeyPair ed25519KeyPair() { + return rawKeyPair("ED25519"); + } + + Bytes ed25519Public(std::string_view privateKey) { + return rawPublicFromPrivate(EVP_PKEY_ED25519, privateKey, "Ed25519"); + } + + Bytes ed25519Sign(std::string_view privateKey, std::string_view message) { + auto key = rawPrivateKey(EVP_PKEY_ED25519, privateKey, "Ed25519"); + EvpMdCtxHandle ctx(EVP_MD_CTX_new(), EVP_MD_CTX_free); + if (!ctx || EVP_DigestSignInit( + ctx.get(), nullptr, nullptr, nullptr, key.get() + ) != 1) { + fail("Ed25519 signing initialization failed"); + } + std::size_t size = 0; + if (EVP_DigestSign( + ctx.get(), nullptr, &size, bytes(message), message.size() + ) != 1) { + fail("Ed25519 signature size query failed"); + } + Bytes signature(size); + if (EVP_DigestSign( + ctx.get(), + signature.data(), + &size, + bytes(message), + message.size() + ) != 1) { + fail("Ed25519 signing failed"); + } + signature.resize(size); + return signature; + } + + bool ed25519Verify( + std::string_view publicKey, + std::string_view message, + std::string_view signature + ) { + if (signature.size() != 64) { + throw Error( + ErrorCode::InvalidSignature, + "Ed25519 signature must be 64 bytes" + ); + } + auto key = rawPublicKey(EVP_PKEY_ED25519, publicKey, "Ed25519"); + EvpMdCtxHandle ctx(EVP_MD_CTX_new(), EVP_MD_CTX_free); + if (!ctx || EVP_DigestVerifyInit( + ctx.get(), nullptr, nullptr, nullptr, key.get() + ) != 1) { + fail("Ed25519 verification initialization failed"); + } + const int result = EVP_DigestVerify( + ctx.get(), + bytes(signature), + signature.size(), + bytes(message), + message.size() + ); + if (result < 0) { + throw Error( + ErrorCode::InvalidSignature, + opensslError("invalid Ed25519 signature") + ); + } + if (result == 0) ERR_clear_error(); + return result == 1; + } + + KeyPair ecdsaKeyPair(std::string_view curve) { + const std::size_t privateSize = curvePrivateSize(curve); + EvpPkeyHandle key( + EVP_PKEY_Q_keygen(nullptr, nullptr, "EC", curveName(curve)), + EVP_PKEY_free + ); + if (!key) fail("ECDSA key generation failed"); + BIGNUM* rawPrivate = nullptr; + if (EVP_PKEY_get_bn_param( + key.get(), OSSL_PKEY_PARAM_PRIV_KEY, &rawPrivate + ) != 1) { + fail("ECDSA private key export failed"); + } + BnHandle privateKey(rawPrivate, BN_free); + KeyPair result {Bytes(privateSize), {}}; + if (BN_bn2binpad( + privateKey.get(), + result.privateKey.data(), + checkedSize(privateSize, "private key") + ) != static_cast(privateSize)) { + fail("ECDSA private key encoding failed"); + } + std::size_t publicSize = 0; + if (EVP_PKEY_get_octet_string_param( + key.get(), OSSL_PKEY_PARAM_PUB_KEY, nullptr, 0, &publicSize + ) != 1) { + fail("ECDSA public key size query failed"); + } + result.publicKey.resize(publicSize); + if (EVP_PKEY_get_octet_string_param( + key.get(), + OSSL_PKEY_PARAM_PUB_KEY, + result.publicKey.data(), + result.publicKey.size(), + &publicSize + ) != 1) { + fail("ECDSA public key export failed"); + } + result.publicKey.resize(publicSize); + return result; + } + + Bytes ecdsaPublic(std::string_view curve, std::string_view privateKey) { + return ecPrivateKey(curve, privateKey).publicKey; + } + + Bytes ecdsaSign( + std::string_view curve, + std::string_view privateKey, + std::string_view message, + std::string_view hash + ) { + auto key = ecPrivateKey(curve, privateKey); + return signDigest(key.key.get(), getShaDigest(hash), message); + } + + bool ecdsaVerify( + std::string_view curve, + std::string_view publicKey, + std::string_view message, + std::string_view signature, + std::string_view hash + ) { + auto key = makeEcdsaPublicKey(curve, publicKey); + return verifyDigestSignature( + key.get(), getShaDigest(hash), message, signature, 0, 0 + ); + } + + bool rsaPkcs1Verify( + std::string_view hash, + std::string_view modulus, + std::string_view exponent, + std::string_view message, + std::string_view signature + ) { + auto key = makeRsaPublicKey(modulus, exponent); + return verifyDigestSignature( + key.get(), + getShaDigest(hash), + message, + signature, + RSA_PKCS1_PADDING, + 0 + ); + } + + bool rsaPssVerify( + std::string_view hash, + std::string_view modulus, + std::string_view exponent, + std::string_view message, + std::string_view signature, + int saltLength + ) { + if (saltLength < -1) { + throw Error( + ErrorCode::InvalidArgument, + "RSA-PSS salt length must be -1 or non-negative" + ); + } + auto key = makeRsaPublicKey(modulus, exponent); + return verifyDigestSignature( + key.get(), + getShaDigest(hash), + message, + signature, + RSA_PKCS1_PSS_PADDING, + saltLength == -1 ? RSA_PSS_SALTLEN_DIGEST : saltLength + ); + } + + KeyPair x25519KeyPair() { + return rawKeyPair("X25519"); + } + + Bytes x25519Public(std::string_view privateKey) { + return rawPublicFromPrivate(EVP_PKEY_X25519, privateKey, "X25519"); + } + + Bytes x25519(std::string_view privateKey, std::string_view peerPublicKey) { + auto key = rawPrivateKey(EVP_PKEY_X25519, privateKey, "X25519"); + auto peer = rawPublicKey(EVP_PKEY_X25519, peerPublicKey, "X25519"); + EvpPkeyCtxHandle ctx( + EVP_PKEY_CTX_new(key.get(), nullptr), EVP_PKEY_CTX_free + ); + if (!ctx || EVP_PKEY_derive_init(ctx.get()) != 1 || + EVP_PKEY_derive_set_peer(ctx.get(), peer.get()) != 1) { + fail("X25519 key exchange initialization failed"); + } + Bytes output(32); + std::size_t size = output.size(); + if (EVP_PKEY_derive(ctx.get(), output.data(), &size) != 1) { + fail("X25519 key exchange failed"); + } + output.resize(size); + const Bytes zero(output.size(), 0); + if (constantTimeEqual( + std::string_view( + reinterpret_cast(output.data()), output.size() + ), + std::string_view( + reinterpret_cast(zero.data()), zero.size() + ) + )) { + throw Error( + ErrorCode::InvalidKey, + "X25519 produced an all-zero shared secret" + ); + } + return output; + } + + Bytes p256Public(std::string_view privateKey) { + return ecPrivateKey("P-256", privateKey).publicKey; + } + + Bytes p256Shared( + std::string_view privateKey, std::string_view peerPublicKey + ) { + if (peerPublicKey.size() != 65 || + static_cast(peerPublicKey[0]) != 0x04) { + throw Error( + ErrorCode::InvalidKey, + "P-256 public key must be a 65-byte uncompressed SEC1 point" + ); + } + auto key = ecPrivateKey("P-256", privateKey); + auto peer = makeEcdsaPublicKey("P-256", peerPublicKey); + EvpPkeyCtxHandle ctx( + EVP_PKEY_CTX_new(key.key.get(), nullptr), EVP_PKEY_CTX_free + ); + if (!ctx || EVP_PKEY_derive_init(ctx.get()) <= 0 || + EVP_PKEY_derive_set_peer(ctx.get(), peer.get()) <= 0) { + fail("P-256 key exchange initialization failed"); + } + std::size_t size = 0; + if (EVP_PKEY_derive(ctx.get(), nullptr, &size) <= 0) { + fail("P-256 shared secret size query failed"); + } + Bytes output(size); + if (EVP_PKEY_derive(ctx.get(), output.data(), &size) <= 0) { + fail("P-256 key exchange failed"); + } + output.resize(size); + return output; + } + + Bytes aesGcmEncrypt( + std::string_view key, + std::string_view nonce, + std::string_view aad, + std::string_view plaintext + ) { + return aeadEncrypt( + aesGcmCipher(key.size()), key, nonce, aad, plaintext + ); + } + + Bytes aesGcmDecrypt( + std::string_view key, + std::string_view nonce, + std::string_view aad, + std::string_view ciphertextWithTag + ) { + return aeadDecrypt( + aesGcmCipher(key.size()), key, nonce, aad, ciphertextWithTag + ); + } + + Bytes chacha20Poly1305Encrypt( + std::string_view key, + std::string_view nonce, + std::string_view aad, + std::string_view plaintext + ) { + if (key.size() != 32) { + throw Error( + ErrorCode::InvalidKey, "ChaCha20-Poly1305 key must be 32 bytes" + ); + } + if (nonce.size() != 12) { + throw Error( + ErrorCode::InvalidArgument, + "ChaCha20-Poly1305 nonce must be 12 bytes" + ); + } + return aeadEncrypt(EVP_chacha20_poly1305(), key, nonce, aad, plaintext); + } + + Bytes chacha20Poly1305Decrypt( + std::string_view key, + std::string_view nonce, + std::string_view aad, + std::string_view ciphertextWithTag + ) { + if (key.size() != 32) { + throw Error( + ErrorCode::InvalidKey, "ChaCha20-Poly1305 key must be 32 bytes" + ); + } + if (nonce.size() != 12) { + throw Error( + ErrorCode::InvalidArgument, + "ChaCha20-Poly1305 nonce must be 12 bytes" + ); + } + return aeadDecrypt( + EVP_chacha20_poly1305(), key, nonce, aad, ciphertextWithTag + ); + } + + Bytes randomBytes(std::size_t length) { + checkedSize(length, "random byte count"); + Bytes output(length); + if (length != 0 && + RAND_bytes(output.data(), static_cast(length)) != 1) { + fail("secure random generation failed"); + } + return output; + } + + bool constantTimeEqual(std::string_view left, std::string_view right) { + return left.size() == right.size() && + (left.empty() || + CRYPTO_memcmp(left.data(), right.data(), left.size()) == 0); + } + + Bytes hkdfExtract( + std::string_view hash, std::string_view salt, std::string_view ikm + ) { + const EVP_MD* md = getShaDigest(hash); + EvpPkeyCtxHandle ctx( + EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, nullptr), EVP_PKEY_CTX_free + ); + Bytes output(static_cast(EVP_MD_size(md))); + std::size_t size = output.size(); + if (!ctx || EVP_PKEY_derive_init(ctx.get()) <= 0 || + EVP_PKEY_CTX_set_hkdf_mode( + ctx.get(), EVP_PKEY_HKDEF_MODE_EXTRACT_ONLY + ) <= 0 || + EVP_PKEY_CTX_set_hkdf_md(ctx.get(), md) <= 0 || + EVP_PKEY_CTX_set1_hkdf_salt( + ctx.get(), bytes(salt), checkedSize(salt.size(), "HKDF salt") + ) <= 0 || + EVP_PKEY_CTX_set1_hkdf_key( + ctx.get(), + bytes(ikm), + checkedSize(ikm.size(), "HKDF input key material") + ) <= 0 || + EVP_PKEY_derive(ctx.get(), output.data(), &size) <= 0) { + fail("HKDF extract failed"); + } + output.resize(size); + return output; + } + + Bytes hkdfExpand( + std::string_view hash, + std::string_view prk, + std::string_view info, + std::size_t length + ) { + const EVP_MD* md = getShaDigest(hash); + const std::size_t maxLength = + static_cast(EVP_MD_size(md)) * 255; + if (length > maxLength) { + throw Error( + ErrorCode::OutputTooLarge, + "HKDF output exceeds 255 digest blocks" + ); + } + EvpPkeyCtxHandle ctx( + EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, nullptr), EVP_PKEY_CTX_free + ); + Bytes output(length); + std::size_t size = output.size(); + if (!ctx || EVP_PKEY_derive_init(ctx.get()) <= 0 || + EVP_PKEY_CTX_set_hkdf_mode( + ctx.get(), EVP_PKEY_HKDEF_MODE_EXPAND_ONLY + ) <= 0 || + EVP_PKEY_CTX_set_hkdf_md(ctx.get(), md) <= 0 || + EVP_PKEY_CTX_set1_hkdf_key( + ctx.get(), + bytes(prk), + checkedSize(prk.size(), "HKDF pseudorandom key") + ) <= 0 || + (!info.empty() && + EVP_PKEY_CTX_add1_hkdf_info( + ctx.get(), bytes(info), checkedSize(info.size(), "HKDF info") + ) <= 0) || + (length != 0 && + EVP_PKEY_derive(ctx.get(), output.data(), &size) <= 0)) { + fail("HKDF expand failed"); + } + output.resize(size); + return output; + } + + Bytes pbkdf2( + std::string_view hash, + std::string_view password, + std::string_view salt, + std::uint32_t iterations, + std::size_t length + ) { + if (iterations == 0 || + iterations > static_cast(INT_MAX)) { + throw Error( + ErrorCode::InvalidArgument, + "PBKDF2 iterations must be between 1 and INT_MAX" + ); + } + const EVP_MD* md = getShaDigest(hash); + const int outputSize = checkedSize(length, "PBKDF2 output"); + Bytes output(length); + if (length != 0 && PKCS5_PBKDF2_HMAC( + password.data(), + checkedSize(password.size(), "password"), + bytes(salt), + checkedSize(salt.size(), "salt"), + static_cast(iterations), + md, + outputSize, + output.data() + ) != 1) { + fail("PBKDF2 failed"); + } + return output; + } + + Bytes scrypt( + std::string_view password, + std::string_view salt, + std::uint64_t n, + std::uint64_t r, + std::uint64_t p, + std::size_t length, + std::uint64_t maxMemory + ) { + if (n <= 1 || (n & (n - 1)) != 0 || r == 0 || p == 0) { + throw Error( + ErrorCode::InvalidArgument, + "scrypt requires power-of-two N > 1 and positive r and p" + ); + } + checkedSize(length, "scrypt output"); + Bytes output(length); + if (length != 0 && EVP_PBE_scrypt( + password.data(), + password.size(), + bytes(salt), + salt.size(), + n, + r, + p, + maxMemory, + output.data(), + output.size() + ) != 1) { + fail("scrypt failed"); + } + return output; + } + + const char* backendVersion() { + return OpenSSL_version(OPENSSL_VERSION); + } + + Features features() { + auto hasDigest = [](const char* name) { + EVP_MD* algorithm = EVP_MD_fetch(nullptr, name, nullptr); + const bool available = algorithm != nullptr; + EVP_MD_free(algorithm); + ERR_clear_error(); + return available; + }; + auto hasCipher = [](const char* name) { + EVP_CIPHER* algorithm = EVP_CIPHER_fetch(nullptr, name, nullptr); + const bool available = algorithm != nullptr; + EVP_CIPHER_free(algorithm); + ERR_clear_error(); + return available; + }; + auto hasKeyAlgorithm = [](const char* name) { + EvpPkeyCtxHandle ctx( + EVP_PKEY_CTX_new_from_name(nullptr, name, nullptr), + EVP_PKEY_CTX_free + ); + const bool available = ctx != nullptr; + ERR_clear_error(); + return available; + }; + auto hasKdf = [](const char* name) { + EVP_KDF* algorithm = EVP_KDF_fetch(nullptr, name, nullptr); + const bool available = algorithm != nullptr; + EVP_KDF_free(algorithm); + ERR_clear_error(); + return available; + }; + EVP_MAC* hmacAlgorithm = EVP_MAC_fetch(nullptr, "HMAC", nullptr); + const bool hasHmac = hmacAlgorithm != nullptr; + EVP_MAC_free(hmacAlgorithm); + ERR_clear_error(); + const bool hasEc = hasKeyAlgorithm("EC"); + return { + hasDigest("SHA256"), + hasDigest("SHA384"), + hasDigest("SHA512"), + hasDigest("MD5"), + hasHmac, + hasKeyAlgorithm("ED25519"), + hasEc, + hasKeyAlgorithm("RSA"), + hasKeyAlgorithm("X25519"), + hasEc, + hasCipher("AES-128-GCM"), + hasCipher("AES-256-GCM"), + hasCipher("CHACHA20-POLY1305"), + RAND_status() == 1, + hasKdf("HKDF"), + hasKdf("PBKDF2"), + hasKdf("SCRYPT") + }; + } +} diff --git a/src/crypto/Crypto.hpp b/src/crypto/Crypto.hpp new file mode 100644 index 000000000..edcdcea6b --- /dev/null +++ b/src/crypto/Crypto.hpp @@ -0,0 +1,185 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace crypto { + constexpr int API_VERSION = 1; + using Bytes = std::vector; + + enum class ErrorCode { + InvalidArgument, + InvalidKey, + InvalidSignature, + AuthenticationFailed, + UnsupportedAlgorithm, + OutputTooLarge, + InvalidState, + BackendError + }; + + class Error : public std::runtime_error { + public: + Error(ErrorCode code, const std::string& message); + ErrorCode code() const noexcept; + const char* codeName() const noexcept; + private: + ErrorCode errorCode; + }; + + struct KeyPair { + Bytes privateKey; + Bytes publicKey; + }; + + struct Features { + bool sha256; + bool sha384; + bool sha512; + bool md5; + bool hmac; + bool ed25519; + bool ecdsa; + bool rsa; + bool x25519; + bool p256; + bool aes128Gcm; + bool aes256Gcm; + bool chacha20Poly1305; + bool random; + bool hkdf; + bool pbkdf2; + bool scrypt; + }; + + class HashContext { + public: + explicit HashContext(std::string_view hash); + ~HashContext(); + HashContext(HashContext&&) noexcept; + HashContext& operator=(HashContext&&) noexcept; + HashContext(const HashContext&) = delete; + HashContext& operator=(const HashContext&) = delete; + + void update(std::string_view data); + Bytes final(); + void reset(); + private: + struct Impl; + std::unique_ptr impl; + }; + + Bytes digest(std::string_view hash, std::string_view data); + Bytes hmac( + std::string_view hash, std::string_view key, std::string_view data + ); + + KeyPair ed25519KeyPair(); + Bytes ed25519Public(std::string_view privateKey); + Bytes ed25519Sign(std::string_view privateKey, std::string_view message); + + bool ed25519Verify( + std::string_view publicKey, + std::string_view message, + std::string_view signature + ); + bool ecdsaVerify( + std::string_view curve, + std::string_view publicKey, + std::string_view message, + std::string_view signature, + std::string_view hash + ); + KeyPair ecdsaKeyPair(std::string_view curve); + Bytes ecdsaPublic(std::string_view curve, std::string_view privateKey); + Bytes ecdsaSign( + std::string_view curve, + std::string_view privateKey, + std::string_view message, + std::string_view hash + ); + bool rsaPkcs1Verify( + std::string_view hash, + std::string_view modulus, + std::string_view exponent, + std::string_view message, + std::string_view signature + ); + bool rsaPssVerify( + std::string_view hash, + std::string_view modulus, + std::string_view exponent, + std::string_view message, + std::string_view signature, + int saltLength + ); + + KeyPair x25519KeyPair(); + Bytes x25519Public(std::string_view privateKey); + Bytes x25519(std::string_view privateKey, std::string_view peerPublicKey); + Bytes p256Public(std::string_view privateKey); + Bytes p256Shared( + std::string_view privateKey, std::string_view peerPublicKey + ); + + Bytes aesGcmEncrypt( + std::string_view key, + std::string_view nonce, + std::string_view aad, + std::string_view plaintext + ); + Bytes aesGcmDecrypt( + std::string_view key, + std::string_view nonce, + std::string_view aad, + std::string_view ciphertextWithTag + ); + Bytes chacha20Poly1305Encrypt( + std::string_view key, + std::string_view nonce, + std::string_view aad, + std::string_view plaintext + ); + Bytes chacha20Poly1305Decrypt( + std::string_view key, + std::string_view nonce, + std::string_view aad, + std::string_view ciphertextWithTag + ); + + Bytes randomBytes(std::size_t length); + bool constantTimeEqual(std::string_view left, std::string_view right); + Bytes hkdfExtract( + std::string_view hash, std::string_view salt, std::string_view ikm + ); + Bytes hkdfExpand( + std::string_view hash, + std::string_view prk, + std::string_view info, + std::size_t length + ); + Bytes pbkdf2( + std::string_view hash, + std::string_view password, + std::string_view salt, + std::uint32_t iterations, + std::size_t length + ); + Bytes scrypt( + std::string_view password, + std::string_view salt, + std::uint64_t n, + std::uint64_t r, + std::uint64_t p, + std::size_t length, + std::uint64_t maxMemory = 0 + ); + + const char* backendVersion(); + Features features(); +} diff --git a/src/logic/scripting/lua/libs/api_lua.hpp b/src/logic/scripting/lua/libs/api_lua.hpp index 965cd0f9c..727265a3a 100644 --- a/src/logic/scripting/lua/libs/api_lua.hpp +++ b/src/logic/scripting/lua/libs/api_lua.hpp @@ -25,6 +25,7 @@ extern const luaL_Reg byteutillib[]; extern const luaL_Reg cameralib[]; extern const luaL_Reg consolelib[]; extern const luaL_Reg corelib[]; +extern const luaL_Reg cryptolib[]; extern const luaL_Reg entitylib[]; extern const luaL_Reg filelib[]; extern const luaL_Reg generationlib[]; @@ -93,3 +94,4 @@ namespace lua { } void initialize_libs_extends(lua::State* L); +void initialize_cryptolib(lua::State* L); diff --git a/src/logic/scripting/lua/libs/libcrypto.cpp b/src/logic/scripting/lua/libs/libcrypto.cpp new file mode 100644 index 000000000..0b60888d7 --- /dev/null +++ b/src/logic/scripting/lua/libs/libcrypto.cpp @@ -0,0 +1,540 @@ +#include +#include +#include +#include +#include +#include + +#include "api_lua.hpp" +#include "crypto/Crypto.hpp" + +namespace { + using HashContext = crypto::HashContext; + + constexpr const char* HASH_CONTEXT_METATABLE = + "voxelcore.crypto.HashContext"; + + int pushBytes(lua::State* L, const crypto::Bytes& value) { + return lua::pushlstring(L, value.data(), value.size()); + } + + int pushFailure(lua::State* L, bool nil, const char* error) { + nil ? lua::pushnil(L) : lua::pushboolean(L, false); + lua::pushstring(L, error); + return 2; + } + + int pushKeyPair(lua::State* L, const crypto::KeyPair& keyPair) { + pushBytes(L, keyPair.privateKey); + pushBytes(L, keyPair.publicKey); + return 2; + } + + template + int verify(lua::State* L, Operation operation) { + try { + if (operation()) return lua::pushboolean(L, true); + return pushFailure(L, false, "invalid_signature"); + } catch (const crypto::Error& error) { + return pushFailure(L, false, error.codeName()); + } catch (const std::exception& error) { + return pushFailure(L, false, "backend_error"); + } + } + + template + int decrypt(lua::State* L, Operation operation) { + try { + return pushBytes(L, operation()); + } catch (const crypto::Error& error) { + return pushFailure(L, true, error.codeName()); + } catch (const std::exception& error) { + return pushFailure(L, true, "backend_error"); + } + } + + std::size_t requireSize(lua::State* L, int index, const char* name) { + if (lua::type(L, index) != LUA_TNUMBER) { + throw std::runtime_error(std::string(name) + " must be a number"); + } + const lua::Number number = lua::tonumber(L, index); + const lua::Integer value = lua::tointeger(L, index); + if (number != static_cast(value)) { + throw std::runtime_error(std::string(name) + " must be an integer"); + } + if (value < 0) { + throw std::runtime_error( + std::string(name) + " must be non-negative" + ); + } + return static_cast(value); + } + + int requireInt(lua::State* L, int index, const char* name) { + if (lua::type(L, index) != LUA_TNUMBER) { + throw std::runtime_error(std::string(name) + " must be a number"); + } + const lua::Number number = lua::tonumber(L, index); + const lua::Integer value = lua::tointeger(L, index); + if (number != static_cast(value)) { + throw std::runtime_error(std::string(name) + " must be an integer"); + } + if (value < INT_MIN || value > INT_MAX) { + throw std::runtime_error(std::string(name) + " is out of range"); + } + return static_cast(value); + } + + int hash(lua::State* L, std::string_view name) { + lua::check_argc(L, 1); + return pushBytes(L, crypto::digest(name, lua::require_lstring(L, 1))); + } + + int l_sha256(lua::State* L) { + return hash(L, "SHA256"); + } + int l_sha384(lua::State* L) { + return hash(L, "SHA384"); + } + int l_sha512(lua::State* L) { + return hash(L, "SHA512"); + } + int l_md5(lua::State* L) { + return hash(L, "MD5"); + } + + int l_hash(lua::State* L) { + lua::check_argc(L, 2); + return pushBytes( + L, + crypto::digest( + lua::require_lstring(L, 1), lua::require_lstring(L, 2) + ) + ); + } + + crypto::HashContext* requireHashContext(lua::State* L) { + return static_cast( + luaL_checkudata(L, 1, HASH_CONTEXT_METATABLE) + ); + } + + int l_hash_context_gc(lua::State* L) { + requireHashContext(L)->~HashContext(); + return 0; + } + + int l_hash_context_update(lua::State* L) { + lua::check_argc(L, 2); + requireHashContext(L)->update(lua::require_lstring(L, 2)); + lua::pushvalue(L, 1); + return 1; + } + + int l_hash_context_final(lua::State* L) { + lua::check_argc(L, 1); + return pushBytes(L, requireHashContext(L)->final()); + } + + int l_hash_context_reset(lua::State* L) { + lua::check_argc(L, 1); + requireHashContext(L)->reset(); + lua::pushvalue(L, 1); + return 1; + } + + void ensureHashContextMetatable(lua::State* L) { + if (luaL_newmetatable(L, HASH_CONTEXT_METATABLE)) { + lua::pushcfunction(L, lua::wrap); + lua::setfield(L, "__gc"); + lua::pushcfunction(L, lua::wrap); + lua::setfield(L, "update"); + lua::pushcfunction(L, lua::wrap); + lua::setfield(L, "final"); + lua::pushcfunction(L, lua::wrap); + lua::setfield(L, "reset"); + lua::pushvalue(L, -1); + lua::setfield(L, "__index"); + } + lua::pop(L); + } + + int l_hash_new(lua::State* L) { + lua::check_argc(L, 1); + const auto name = lua::require_lstring(L, 1); + ensureHashContextMetatable(L); + void* memory = lua_newuserdata(L, sizeof(crypto::HashContext)); + new (memory) crypto::HashContext(name); + luaL_getmetatable(L, HASH_CONTEXT_METATABLE); + lua_setmetatable(L, -2); + return 1; + } + + int l_hmac(lua::State* L) { + lua::check_argc(L, 3); + return pushBytes( + L, + crypto::hmac( + lua::require_lstring(L, 1), + lua::require_lstring(L, 2), + lua::require_lstring(L, 3) + ) + ); + } + + int l_ed25519_verify(lua::State* L) { + lua::check_argc(L, 3); + const auto key = lua::require_lstring(L, 1); + const auto message = lua::require_lstring(L, 2); + const auto signature = lua::require_lstring(L, 3); + return verify(L, [=] { + return crypto::ed25519Verify(key, message, signature); + }); + } + + int l_ed25519_keypair(lua::State* L) { + lua::check_argc(L, 0); + return pushKeyPair(L, crypto::ed25519KeyPair()); + } + + int l_ed25519_public(lua::State* L) { + lua::check_argc(L, 1); + return pushBytes(L, crypto::ed25519Public(lua::require_lstring(L, 1))); + } + + int l_ed25519_sign(lua::State* L) { + lua::check_argc(L, 2); + return pushBytes( + L, + crypto::ed25519Sign( + lua::require_lstring(L, 1), lua::require_lstring(L, 2) + ) + ); + } + + int l_ecdsa_verify(lua::State* L) { + lua::check_argc(L, 5); + const auto curve = lua::require_lstring(L, 1); + const auto key = lua::require_lstring(L, 2); + const auto message = lua::require_lstring(L, 3); + const auto signature = lua::require_lstring(L, 4); + const auto hashName = lua::require_lstring(L, 5); + return verify(L, [=] { + return crypto::ecdsaVerify( + curve, key, message, signature, hashName + ); + }); + } + + int l_ecdsa_keypair(lua::State* L) { + lua::check_argc(L, 1); + return pushKeyPair(L, crypto::ecdsaKeyPair(lua::require_lstring(L, 1))); + } + + int l_ecdsa_public(lua::State* L) { + lua::check_argc(L, 2); + return pushBytes( + L, + crypto::ecdsaPublic( + lua::require_lstring(L, 1), lua::require_lstring(L, 2) + ) + ); + } + + int l_ecdsa_sign(lua::State* L) { + lua::check_argc(L, 4); + return pushBytes( + L, + crypto::ecdsaSign( + lua::require_lstring(L, 1), + lua::require_lstring(L, 2), + lua::require_lstring(L, 3), + lua::require_lstring(L, 4) + ) + ); + } + + int l_rsa_pkcs1_verify(lua::State* L) { + lua::check_argc(L, 5); + const auto hashName = lua::require_lstring(L, 1); + const auto modulus = lua::require_lstring(L, 2); + const auto exponent = lua::require_lstring(L, 3); + const auto message = lua::require_lstring(L, 4); + const auto signature = lua::require_lstring(L, 5); + return verify(L, [=] { + return crypto::rsaPkcs1Verify( + hashName, modulus, exponent, message, signature + ); + }); + } + + int l_rsa_pss_verify(lua::State* L) { + lua::check_argc(L, 6); + const auto hashName = lua::require_lstring(L, 1); + const auto modulus = lua::require_lstring(L, 2); + const auto exponent = lua::require_lstring(L, 3); + const auto message = lua::require_lstring(L, 4); + const auto signature = lua::require_lstring(L, 5); + const int saltLength = requireInt(L, 6, "salt_length"); + return verify(L, [=] { + return crypto::rsaPssVerify( + hashName, modulus, exponent, message, signature, saltLength + ); + }); + } + + int l_x25519_public(lua::State* L) { + lua::check_argc(L, 1); + return pushBytes(L, crypto::x25519Public(lua::require_lstring(L, 1))); + } + + int l_x25519_keypair(lua::State* L) { + lua::check_argc(L, 0); + return pushKeyPair(L, crypto::x25519KeyPair()); + } + + int l_x25519(lua::State* L) { + lua::check_argc(L, 2); + return pushBytes( + L, + crypto::x25519( + lua::require_lstring(L, 1), lua::require_lstring(L, 2) + ) + ); + } + + int l_p256_public(lua::State* L) { + lua::check_argc(L, 1); + return pushBytes(L, crypto::p256Public(lua::require_lstring(L, 1))); + } + + int l_p256_keypair(lua::State* L) { + lua::check_argc(L, 0); + return pushKeyPair(L, crypto::ecdsaKeyPair("P-256")); + } + + int l_p256_shared(lua::State* L) { + lua::check_argc(L, 2); + return pushBytes( + L, + crypto::p256Shared( + lua::require_lstring(L, 1), lua::require_lstring(L, 2) + ) + ); + } + + int l_aes_gcm_encrypt(lua::State* L) { + lua::check_argc(L, 4); + return pushBytes( + L, + crypto::aesGcmEncrypt( + lua::require_lstring(L, 1), + lua::require_lstring(L, 2), + lua::require_lstring(L, 3), + lua::require_lstring(L, 4) + ) + ); + } + + int l_aes_gcm_decrypt(lua::State* L) { + lua::check_argc(L, 4); + const auto key = lua::require_lstring(L, 1); + const auto nonce = lua::require_lstring(L, 2); + const auto aad = lua::require_lstring(L, 3); + const auto ciphertext = lua::require_lstring(L, 4); + return decrypt(L, [=] { + return crypto::aesGcmDecrypt(key, nonce, aad, ciphertext); + }); + } + + int l_chacha20_poly1305_encrypt(lua::State* L) { + lua::check_argc(L, 4); + return pushBytes( + L, + crypto::chacha20Poly1305Encrypt( + lua::require_lstring(L, 1), + lua::require_lstring(L, 2), + lua::require_lstring(L, 3), + lua::require_lstring(L, 4) + ) + ); + } + + int l_chacha20_poly1305_decrypt(lua::State* L) { + lua::check_argc(L, 4); + const auto key = lua::require_lstring(L, 1); + const auto nonce = lua::require_lstring(L, 2); + const auto aad = lua::require_lstring(L, 3); + const auto ciphertext = lua::require_lstring(L, 4); + return decrypt(L, [=] { + return crypto::chacha20Poly1305Decrypt(key, nonce, aad, ciphertext); + }); + } + + int l_random_bytes(lua::State* L) { + lua::check_argc(L, 1); + return pushBytes(L, crypto::randomBytes(requireSize(L, 1, "length"))); + } + + int l_constant_time_equal(lua::State* L) { + lua::check_argc(L, 2); + return lua::pushboolean( + L, + crypto::constantTimeEqual( + lua::require_lstring(L, 1), lua::require_lstring(L, 2) + ) + ); + } + + int l_hkdf_extract(lua::State* L) { + lua::check_argc(L, 3); + return pushBytes( + L, + crypto::hkdfExtract( + lua::require_lstring(L, 1), + lua::require_lstring(L, 2), + lua::require_lstring(L, 3) + ) + ); + } + + int l_hkdf_expand(lua::State* L) { + lua::check_argc(L, 4); + return pushBytes( + L, + crypto::hkdfExpand( + lua::require_lstring(L, 1), + lua::require_lstring(L, 2), + lua::require_lstring(L, 3), + requireSize(L, 4, "length") + ) + ); + } + + int l_pbkdf2(lua::State* L) { + lua::check_argc(L, 5); + const auto iterations = requireSize(L, 4, "iterations"); + if (iterations == 0 || iterations > UINT32_MAX) { + throw std::runtime_error( + "iterations must be between 1 and 4294967295" + ); + } + return pushBytes( + L, + crypto::pbkdf2( + lua::require_lstring(L, 1), + lua::require_lstring(L, 2), + lua::require_lstring(L, 3), + static_cast(iterations), + requireSize(L, 5, "length") + ) + ); + } + + int l_scrypt(lua::State* L) { + const auto argc = lua::check_argc(L, 6, 7); + const auto maxMemory = + argc == 7 ? requireSize(L, 7, "max_memory") : std::size_t {0}; + return pushBytes( + L, + crypto::scrypt( + lua::require_lstring(L, 1), + lua::require_lstring(L, 2), + requireSize(L, 3, "n"), + requireSize(L, 4, "r"), + requireSize(L, 5, "p"), + requireSize(L, 6, "length"), + maxMemory + ) + ); + } + + void setFeature(lua::State* L, const char* name, bool available) { + lua::pushboolean(L, available); + lua::setfield(L, name); + } + + int l_features(lua::State* L) { + lua::check_argc(L, 0); + const auto features = crypto::features(); + lua::createtable(L, 0, 26); + lua::pushstring(L, "OpenSSL"); + lua::setfield(L, "backend"); + lua::pushstring(L, crypto::backendVersion()); + lua::setfield(L, "backend_version"); + lua::pushinteger(L, crypto::API_VERSION); + lua::setfield(L, "api_version"); + setFeature(L, "sha256", features.sha256); + setFeature(L, "sha384", features.sha384); + setFeature(L, "sha512", features.sha512); + setFeature(L, "md5", features.md5); + setFeature(L, "hmac", features.hmac); + setFeature(L, "streaming_hash", features.sha256); + setFeature(L, "ed25519", features.ed25519); + setFeature(L, "ed25519_verify", features.ed25519); + setFeature(L, "ecdsa", features.ecdsa); + setFeature(L, "ecdsa_verify", features.ecdsa); + setFeature(L, "rsa_pkcs1_verify", features.rsa); + setFeature(L, "rsa_pss_verify", features.rsa); + setFeature(L, "x25519", features.x25519); + setFeature(L, "p256", features.p256); + setFeature(L, "aes_128_gcm", features.aes128Gcm); + setFeature(L, "aes_256_gcm", features.aes256Gcm); + setFeature(L, "aes_gcm", features.aes128Gcm && features.aes256Gcm); + setFeature(L, "chacha20_poly1305", features.chacha20Poly1305); + setFeature(L, "random_bytes", features.random); + setFeature(L, "constant_time_equal", true); + setFeature(L, "hkdf", features.hkdf); + setFeature(L, "pbkdf2", features.pbkdf2); + setFeature(L, "scrypt", features.scrypt); + return 1; + } +} + +const luaL_Reg cryptolib[] = { + {"sha256", lua::wrap}, + {"sha384", lua::wrap}, + {"sha512", lua::wrap}, + {"md5", lua::wrap}, + {"hash", lua::wrap}, + {"hash_new", lua::wrap}, + {"hmac", lua::wrap}, + {"ed25519_keypair", lua::wrap}, + {"ed25519_public", lua::wrap}, + {"ed25519_sign", lua::wrap}, + {"ed25519_verify", lua::wrap}, + {"ecdsa_keypair", lua::wrap}, + {"ecdsa_public", lua::wrap}, + {"ecdsa_sign", lua::wrap}, + {"ecdsa_verify", lua::wrap}, + {"rsa_pkcs1_verify", lua::wrap}, + {"rsa_pss_verify", lua::wrap}, + {"x25519_keypair", lua::wrap}, + {"x25519_public", lua::wrap}, + {"x25519", lua::wrap}, + {"x25519_shared", lua::wrap}, + {"p256_keypair", lua::wrap}, + {"p256_public", lua::wrap}, + {"p256_shared", lua::wrap}, + {"aes_gcm_encrypt", lua::wrap}, + {"aes_gcm_decrypt", lua::wrap}, + {"chacha20_poly1305_encrypt", lua::wrap}, + {"chacha20_poly1305_decrypt", lua::wrap}, + {"random_bytes", lua::wrap}, + {"constant_time_equal", lua::wrap}, + {"hkdf_extract", lua::wrap}, + {"hkdf_expand", lua::wrap}, + {"pbkdf2", lua::wrap}, + {"scrypt", lua::wrap}, + {"features", lua::wrap}, + {NULL, NULL} +}; + +void initialize_cryptolib(lua::State* L) { + ensureHashContextMetatable(L); + if (lua::getglobal(L, "crypto")) { + lua::pushinteger(L, crypto::API_VERSION); + lua::setfield(L, "API_VERSION"); + lua::pop(L); + } +} diff --git a/src/logic/scripting/lua/lua_engine.cpp b/src/logic/scripting/lua/lua_engine.cpp index 850b34109..383e7669b 100644 --- a/src/logic/scripting/lua/lua_engine.cpp +++ b/src/logic/scripting/lua/lua_engine.cpp @@ -53,6 +53,8 @@ static void create_libs(State* L, StateType stateType) { openlib(L, "bjson", bjsonlib); openlib(L, "block", blocklib); openlib(L, "byteutil", byteutillib); + openlib(L, "crypto", cryptolib); + initialize_cryptolib(L); openlib(L, "file", filelib); openlib(L, "generation", generationlib); openlib(L, "item", itemlib); diff --git a/test/crypto/Crypto.cpp b/test/crypto/Crypto.cpp new file mode 100644 index 000000000..c4eb90fb3 --- /dev/null +++ b/test/crypto/Crypto.cpp @@ -0,0 +1,470 @@ +#include "crypto/Crypto.hpp" + +#include +#include +#include +#include +#include + +#include +#include + +namespace { + using PkeyHandle = std::unique_ptr; + using MdCtxHandle = std::unique_ptr; + + crypto::Bytes hex(const std::string& value) { + crypto::Bytes result; + result.reserve(value.size() / 2); + for (std::size_t i = 0; i < value.size(); i += 2) { + result.push_back( + static_cast( + std::stoul(value.substr(i, 2), nullptr, 16) + ) + ); + } + return result; + } + + std::string binary(const crypto::Bytes& value) { + return {reinterpret_cast(value.data()), value.size()}; + } + + crypto::Bytes sign( + EVP_PKEY* key, + const EVP_MD* digest, + std::string_view message, + int rsaPadding = 0, + int saltLength = 0 + ) { + MdCtxHandle ctx(EVP_MD_CTX_new(), EVP_MD_CTX_free); + EVP_PKEY_CTX* pkeyCtx = nullptr; + EXPECT_NE(ctx, nullptr); + EXPECT_EQ( + EVP_DigestSignInit(ctx.get(), &pkeyCtx, digest, nullptr, key), 1 + ); + if (rsaPadding != 0) { + EXPECT_GT(EVP_PKEY_CTX_set_rsa_padding(pkeyCtx, rsaPadding), 0); + if (rsaPadding == RSA_PKCS1_PSS_PADDING) { + EXPECT_GT( + EVP_PKEY_CTX_set_rsa_pss_saltlen(pkeyCtx, saltLength), 0 + ); + } + } + EXPECT_EQ( + EVP_DigestSignUpdate(ctx.get(), message.data(), message.size()), 1 + ); + std::size_t size = 0; + EXPECT_EQ(EVP_DigestSignFinal(ctx.get(), nullptr, &size), 1); + crypto::Bytes signature(size); + EXPECT_EQ(EVP_DigestSignFinal(ctx.get(), signature.data(), &size), 1); + signature.resize(size); + return signature; + } + + crypto::Bytes keyInteger(EVP_PKEY* key, const char* name) { + BIGNUM* raw = nullptr; + EXPECT_EQ(EVP_PKEY_get_bn_param(key, name, &raw), 1); + std::unique_ptr value(raw, BN_free); + crypto::Bytes result( + static_cast(BN_num_bytes(value.get())) + ); + BN_bn2bin(value.get(), result.data()); + return result; + } +} + +TEST(Crypto, HashesAndHmacMatchKnownVectors) { + EXPECT_EQ( + crypto::digest("SHA256", "abc"), + hex("ba7816bf8f01cfea414140de5dae2223" + "b00361a396177a9cb410ff61f20015ad") + ); + EXPECT_EQ( + crypto::digest("SHA384", "abc"), + hex("cb00753f45a35e8bb5a03d699ac65007" + "272c32ab0eded1631a8b605a43ff5bed" + "8086072ba1e7cc2358baeca134c825a7") + ); + EXPECT_EQ( + crypto::digest("SHA512", "abc"), + hex("ddaf35a193617abacc417349ae204131" + "12e6fa4e89a97ea20a9eeee64b55d39" + "a2192992a274fc1a836ba3c23a3feebbd" + "454d4423643ce80e2a9ac94fa54ca49f") + ); + EXPECT_EQ( + crypto::digest("MD5", "abc"), hex("900150983cd24fb0d6963f7d28e17f72") + ); + const crypto::Bytes key(20, 0x0b); + EXPECT_EQ( + crypto::hmac("SHA256", binary(key), "Hi There"), + hex("b0344c61d8db38535ca8afceaf0bf12b" + "881dc200c9833da726e9376c2e32cff7") + ); +} + +TEST(Crypto, StreamingHashCanBeResetAndRejectsUpdateAfterFinal) { + crypto::HashContext context("SHA256"); + context.update("a"); + context.update(std::string("b\0c", 3)); + EXPECT_EQ( + context.final(), crypto::digest("SHA256", std::string("ab\0c", 4)) + ); + try { + context.update("more"); + FAIL() << "update after final must fail"; + } catch (const crypto::Error& error) { + EXPECT_EQ(error.code(), crypto::ErrorCode::InvalidState); + } + context.reset(); + context.update("abc"); + EXPECT_EQ(context.final(), crypto::digest("SHA256", "abc")); +} + +TEST(Crypto, Ed25519MatchesRfc8032Vector) { + auto publicKey = + hex("d75a980182b10ab7d54bfed3c964073a" + "0ee172f3daa62325af021a68f707511a"); + auto signature = + hex("e5564300c360ac729086e2cc806e828a" + "84877f1eb8e5d974d873e06522490155" + "5fb8821590a33bacc61e39701cf9b46b" + "d25bf5f0595bbe24655141438e7a100b"); + EXPECT_TRUE( + crypto::ed25519Verify(binary(publicKey), "", binary(signature)) + ); + signature[0] ^= 1; + EXPECT_FALSE( + crypto::ed25519Verify(binary(publicKey), "", binary(signature)) + ); +} + +TEST(Crypto, Ed25519GeneratesKeysAndSigns) { + const auto keyPair = crypto::ed25519KeyPair(); + ASSERT_EQ(keyPair.privateKey.size(), std::size_t {32}); + ASSERT_EQ(keyPair.publicKey.size(), std::size_t {32}); + EXPECT_EQ( + crypto::ed25519Public(binary(keyPair.privateKey)), keyPair.publicKey + ); + const auto signature = + crypto::ed25519Sign(binary(keyPair.privateKey), std::string("a\0b", 3)); + EXPECT_TRUE( + crypto::ed25519Verify( + binary(keyPair.publicKey), std::string("a\0b", 3), binary(signature) + ) + ); +} + +TEST(Crypto, EcdsaVerifiesAllSupportedCurves) { + struct Case { + const char* curve; + const char* opensslCurve; + const EVP_MD* digest; + const char* hash; + }; + const Case cases[] { + {"P-256", "prime256v1", EVP_sha256(), "SHA256"}, + {"P-384", "secp384r1", EVP_sha384(), "SHA384"}, + {"P-521", "secp521r1", EVP_sha512(), "SHA512"} + }; + for (const auto& item : cases) { + SCOPED_TRACE(item.curve); + PkeyHandle key( + EVP_PKEY_Q_keygen(nullptr, nullptr, "EC", item.opensslCurve), + EVP_PKEY_free + ); + ASSERT_NE(key, nullptr); + std::size_t publicKeySize = 0; + ASSERT_EQ( + EVP_PKEY_get_octet_string_param( + key.get(), OSSL_PKEY_PARAM_PUB_KEY, nullptr, 0, &publicKeySize + ), + 1 + ); + crypto::Bytes publicKey(publicKeySize); + ASSERT_EQ( + EVP_PKEY_get_octet_string_param( + key.get(), + OSSL_PKEY_PARAM_PUB_KEY, + publicKey.data(), + publicKey.size(), + &publicKeySize + ), + 1 + ); + auto signature = sign(key.get(), item.digest, "message"); + EXPECT_TRUE( + crypto::ecdsaVerify( + item.curve, + binary(publicKey), + "message", + binary(signature), + item.hash + ) + ); + EXPECT_FALSE( + crypto::ecdsaVerify( + item.curve, + binary(publicKey), + "tampered", + binary(signature), + item.hash + ) + ); + } +} + +TEST(Crypto, EcdsaGeneratesKeysAndSignsOnAllSupportedCurves) { + struct Case { + const char* curve; + const char* hash; + std::size_t privateSize; + std::size_t publicSize; + }; + const Case cases[] { + {"P-256", "SHA256", 32, 65}, + {"P-384", "SHA384", 48, 97}, + {"P-521", "SHA512", 66, 133} + }; + for (const auto& item : cases) { + SCOPED_TRACE(item.curve); + const auto keyPair = crypto::ecdsaKeyPair(item.curve); + EXPECT_EQ(keyPair.privateKey.size(), item.privateSize); + EXPECT_EQ(keyPair.publicKey.size(), item.publicSize); + EXPECT_EQ( + crypto::ecdsaPublic(item.curve, binary(keyPair.privateKey)), + keyPair.publicKey + ); + const auto signature = crypto::ecdsaSign( + item.curve, binary(keyPair.privateKey), "message", item.hash + ); + EXPECT_TRUE( + crypto::ecdsaVerify( + item.curve, + binary(keyPair.publicKey), + "message", + binary(signature), + item.hash + ) + ); + } +} + +TEST(Crypto, RsaVerifiesPkcs1AndPssSignatures) { + PkeyHandle key( + EVP_PKEY_Q_keygen(nullptr, nullptr, "RSA", 2048), EVP_PKEY_free + ); + ASSERT_NE(key, nullptr); + const auto modulus = keyInteger(key.get(), OSSL_PKEY_PARAM_RSA_N); + const auto exponent = keyInteger(key.get(), OSSL_PKEY_PARAM_RSA_E); + + auto pkcs1 = sign(key.get(), EVP_sha256(), "message", RSA_PKCS1_PADDING); + EXPECT_TRUE( + crypto::rsaPkcs1Verify( + "SHA256", + binary(modulus), + binary(exponent), + "message", + binary(pkcs1) + ) + ); + EXPECT_FALSE( + crypto::rsaPkcs1Verify( + "SHA256", + binary(modulus), + binary(exponent), + "tampered", + binary(pkcs1) + ) + ); + + auto pss = sign( + key.get(), + EVP_sha256(), + "message", + RSA_PKCS1_PSS_PADDING, + RSA_PSS_SALTLEN_DIGEST + ); + EXPECT_TRUE( + crypto::rsaPssVerify( + "SHA256", + binary(modulus), + binary(exponent), + "message", + binary(pss), + -1 + ) + ); + EXPECT_FALSE( + crypto::rsaPssVerify( + "SHA256", + binary(modulus), + binary(exponent), + "tampered", + binary(pss), + -1 + ) + ); +} + +TEST(Crypto, X25519MatchesRfc7748PublicKeyVector) { + const auto privateKey = + hex("77076d0a7318a57d3c16c17251b26645" + "df4c2f87ebc0992ab177fba51db92c2a"); + EXPECT_EQ( + crypto::x25519Public(binary(privateKey)), + hex("8520f0098930a754748b7ddcb43ef75a0" + "dbf3a0d26381af4eba4a98eaa9b4e6a") + ); +} + +TEST(Crypto, X25519GeneratedKeyPairsDeriveTheSameSecret) { + const auto alice = crypto::x25519KeyPair(); + const auto bob = crypto::x25519KeyPair(); + EXPECT_EQ(alice.privateKey.size(), std::size_t {32}); + EXPECT_EQ(alice.publicKey.size(), std::size_t {32}); + EXPECT_EQ(crypto::x25519Public(binary(alice.privateKey)), alice.publicKey); + EXPECT_EQ( + crypto::x25519(binary(alice.privateKey), binary(bob.publicKey)), + crypto::x25519(binary(bob.privateKey), binary(alice.publicKey)) + ); +} + +TEST(Crypto, P256DerivesPublicKeyAndSharedSecret) { + crypto::Bytes privateA(32, 0); + crypto::Bytes privateB(32, 0); + privateA.back() = 1; + privateB.back() = 2; + const auto publicA = crypto::p256Public(binary(privateA)); + const auto publicB = crypto::p256Public(binary(privateB)); + EXPECT_EQ( + publicA, + hex("046b17d1f2e12c4247f8bce6e563a440" + "f277037d812deb33a0f4a13945d898c296" + "4fe342e2fe1a7f9b8ee7eb4a7c0f9e16" + "2bce33576b315ececbb6406837bf51f5") + ); + EXPECT_EQ( + crypto::p256Shared(binary(privateA), binary(publicB)), + crypto::p256Shared(binary(privateB), binary(publicA)) + ); +} + +TEST(Crypto, AesGcmMatchesKnownVectorAndRejectsTampering) { + const crypto::Bytes key(16, 0); + const crypto::Bytes nonce(12, 0); + const crypto::Bytes plaintext(16, 0); + auto ciphertext = crypto::aesGcmEncrypt( + binary(key), binary(nonce), "", binary(plaintext) + ); + EXPECT_EQ( + ciphertext, + hex("0388dace60b6a392f328c2b971b2fe78" + "ab6e47d42cec13bdf53a67b21257bddf") + ); + EXPECT_EQ( + crypto::aesGcmDecrypt( + binary(key), binary(nonce), "", binary(ciphertext) + ), + plaintext + ); + ciphertext.back() ^= 1; + EXPECT_THROW( + crypto::aesGcmDecrypt( + binary(key), binary(nonce), "", binary(ciphertext) + ), + std::runtime_error + ); +} + +TEST(Crypto, Chacha20Poly1305RoundTripsAndRejectsTampering) { + const crypto::Bytes key(32, 0); + const crypto::Bytes nonce(12, 0); + auto ciphertext = crypto::chacha20Poly1305Encrypt( + binary(key), binary(nonce), "aad", "plaintext" + ); + EXPECT_EQ( + binary( + crypto::chacha20Poly1305Decrypt( + binary(key), binary(nonce), "aad", binary(ciphertext) + ) + ), + "plaintext" + ); + ciphertext.back() ^= 1; + EXPECT_THROW( + crypto::chacha20Poly1305Decrypt( + binary(key), binary(nonce), "aad", binary(ciphertext) + ), + std::runtime_error + ); +} + +TEST(Crypto, HkdfMatchesRfc5869Vector) { + const crypto::Bytes ikm(22, 0x0b); + const auto salt = hex("000102030405060708090a0b0c"); + const auto info = hex("f0f1f2f3f4f5f6f7f8f9"); + const auto prk = crypto::hkdfExtract("SHA256", binary(salt), binary(ikm)); + EXPECT_EQ( + prk, + hex("077709362c2e32df0ddc3f0dc47bba63" + "90b6c73bb50f9c3122ec844ad7c2b3e5") + ); + EXPECT_EQ( + crypto::hkdfExpand("SHA256", binary(prk), binary(info), 42), + hex("3cb25f25faacd57a90434f64d0362f2a" + "2d2d0a90cf1a5a4c5db02d56ecc4c5bf" + "34007208d5b887185865") + ); +} + +TEST(Crypto, PasswordKdfsMatchKnownVectors) { + EXPECT_EQ( + crypto::pbkdf2("SHA256", "password", "salt", 1, 32), + hex("120fb6cffcf8b32c43e7225256c4f837" + "a86548c92ccc35480805987cb70be17b") + ); + EXPECT_EQ( + crypto::scrypt("", "", 16, 1, 1, 64), + hex("77d6576238657b203b19ca42c18a0497" + "f16b4844e3074ae8dfdffa3fede21442f" + "cd0069ded0948f8326a753a0fc81f17e" + "8d3e0fb2e0d3628cf35e20c38d18906") + ); +} + +TEST(Crypto, ReportsRuntimeCapabilitiesAndStableErrors) { + const auto available = crypto::features(); + EXPECT_TRUE(available.sha256); + EXPECT_TRUE(available.ed25519); + EXPECT_TRUE(available.ecdsa); + EXPECT_TRUE(available.x25519); + EXPECT_TRUE(available.aes128Gcm); + EXPECT_TRUE(available.pbkdf2); + EXPECT_TRUE(available.scrypt); + EXPECT_EQ(crypto::API_VERSION, 1); + + const crypto::Bytes key(16, 0); + const crypto::Bytes nonce(12, 0); + auto ciphertext = + crypto::aesGcmEncrypt(binary(key), binary(nonce), "", "secret"); + ciphertext.back() ^= 1; + try { + crypto::aesGcmDecrypt( + binary(key), binary(nonce), "", binary(ciphertext) + ); + FAIL() << "tampered ciphertext must fail"; + } catch (const crypto::Error& error) { + EXPECT_EQ(error.code(), crypto::ErrorCode::AuthenticationFailed); + EXPECT_STREQ(error.codeName(), "authentication_failed"); + } +} + +TEST(Crypto, UtilitiesHandleBinaryData) { + const std::string binaryValue("a\0b", 3); + EXPECT_TRUE(crypto::constantTimeEqual(binaryValue, binaryValue)); + EXPECT_FALSE(crypto::constantTimeEqual(binaryValue, "a")); + EXPECT_FALSE(crypto::constantTimeEqual("left", "lest")); + EXPECT_EQ(crypto::randomBytes(32).size(), std::size_t {32}); +} diff --git a/test/crypto/CryptoLua.cpp b/test/crypto/CryptoLua.cpp new file mode 100644 index 000000000..3bce6af04 --- /dev/null +++ b/test/crypto/CryptoLua.cpp @@ -0,0 +1,70 @@ +#include + +#include + +#include "logic/scripting/lua/libs/api_lua.hpp" + +namespace { + struct LuaCloser { + void operator()(lua::State* state) const { + lua_close(state); + } + }; +} + +TEST(CryptoLua, PublicV1ApiWorksFromLua) { + std::unique_ptr state(luaL_newstate()); + ASSERT_NE(state, nullptr); + luaL_openlibs(state.get()); + lua::openlib(state.get(), "crypto", cryptolib); + initialize_cryptolib(state.get()); + + const char* script = R"lua( + assert(crypto.API_VERSION == 1) + local features = crypto.features() + assert(features.api_version == 1) + assert(features.backend == "OpenSSL") + + local binary = "a\0b" + assert(crypto.hash("SHA256", binary) == crypto.sha256(binary)) + local stream = crypto.hash_new("SHA256") + assert(stream:update("a"):update("\0b"):final() == crypto.sha256(binary)) + stream:reset():update("abc") + assert(stream:final() == crypto.sha256("abc")) + + local private, public = crypto.ed25519_keypair() + assert(crypto.ed25519_public(private) == public) + local signature = crypto.ed25519_sign(private, binary) + assert(crypto.ed25519_verify(public, binary, signature) == true) + local ok, error_code = crypto.ed25519_verify(public, "changed", signature) + assert(ok == false and error_code == "invalid_signature") + + local x_private_a, x_public_a = crypto.x25519_keypair() + local x_private_b, x_public_b = crypto.x25519_keypair() + assert(crypto.x25519(x_private_a, x_public_b) == + crypto.x25519_shared(x_private_b, x_public_a)) + + local p_private_a, p_public_a = crypto.p256_keypair() + local p_private_b, p_public_b = crypto.p256_keypair() + assert(crypto.p256_public(p_private_a) == p_public_a) + assert(crypto.p256_shared(p_private_a, p_public_b) == + crypto.p256_shared(p_private_b, p_public_a)) + + local key = crypto.random_bytes(16) + local nonce = crypto.random_bytes(12) + local ciphertext = crypto.aes_gcm_encrypt(key, nonce, "aad", binary) + assert(crypto.aes_gcm_decrypt(key, nonce, "aad", ciphertext) == binary) + local plaintext, decrypt_error = crypto.aes_gcm_decrypt( + key, nonce, "changed", ciphertext + ) + assert(plaintext == nil and decrypt_error == "authentication_failed") + + assert(#crypto.pbkdf2("SHA256", "password", "salt", 1, 32) == 32) + assert(#crypto.scrypt("password", "salt", 16, 1, 1, 32) == 32) + )lua"; + + const int result = luaL_dostring(state.get(), script); + if (result != 0) { + FAIL() << lua_tostring(state.get(), -1); + } +} diff --git a/vcpkg.json b/vcpkg.json index 4f56dc6c2..79f2acebe 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -14,6 +14,7 @@ "entt", "gtest", "curl", - "freetype" + "freetype", + "openssl" ] } From e27c13284f119447af815b5608e6cab48b10ece5 Mon Sep 17 00:00:00 2001 From: dagger Date: Sat, 22 Aug 2026 16:07:59 +0700 Subject: [PATCH 02/56] fix p256 public key --- src/crypto/Crypto.cpp | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/src/crypto/Crypto.cpp b/src/crypto/Crypto.cpp index 169f94d65..72f92366a 100644 --- a/src/crypto/Crypto.cpp +++ b/src/crypto/Crypto.cpp @@ -811,23 +811,13 @@ namespace crypto { ) != static_cast(privateSize)) { fail("ECDSA private key encoding failed"); } - std::size_t publicSize = 0; - if (EVP_PKEY_get_octet_string_param( - key.get(), OSSL_PKEY_PARAM_PUB_KEY, nullptr, 0, &publicSize - ) != 1) { - fail("ECDSA public key size query failed"); - } - result.publicKey.resize(publicSize); - if (EVP_PKEY_get_octet_string_param( - key.get(), - OSSL_PKEY_PARAM_PUB_KEY, - result.publicKey.data(), - result.publicKey.size(), - &publicSize - ) != 1) { - fail("ECDSA public key export failed"); - } - result.publicKey.resize(publicSize); + result.publicKey = ecPrivateKey( + curve, + std::string_view( + reinterpret_cast(result.privateKey.data()), + result.privateKey.size() + ) + ).publicKey; return result; } From c65b9ae38524c18b566f143e082677ef8042dd2c Mon Sep 17 00:00:00 2001 From: dagger Date: Sat, 22 Aug 2026 17:14:42 +0700 Subject: [PATCH 03/56] fix ecdsa test on linux --- test/crypto/Crypto.cpp | 41 +++++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/test/crypto/Crypto.cpp b/test/crypto/Crypto.cpp index c4eb90fb3..b9153323b 100644 --- a/test/crypto/Crypto.cpp +++ b/test/crypto/Crypto.cpp @@ -162,11 +162,12 @@ TEST(Crypto, EcdsaVerifiesAllSupportedCurves) { const char* opensslCurve; const EVP_MD* digest; const char* hash; + std::size_t coordinateSize; }; const Case cases[] { - {"P-256", "prime256v1", EVP_sha256(), "SHA256"}, - {"P-384", "secp384r1", EVP_sha384(), "SHA384"}, - {"P-521", "secp521r1", EVP_sha512(), "SHA512"} + {"P-256", "prime256v1", EVP_sha256(), "SHA256", 32}, + {"P-384", "secp384r1", EVP_sha384(), "SHA384", 48}, + {"P-521", "secp521r1", EVP_sha512(), "SHA512", 66} }; for (const auto& item : cases) { SCOPED_TRACE(item.curve); @@ -175,23 +176,31 @@ TEST(Crypto, EcdsaVerifiesAllSupportedCurves) { EVP_PKEY_free ); ASSERT_NE(key, nullptr); - std::size_t publicKeySize = 0; + BIGNUM* rawX = nullptr; + BIGNUM* rawY = nullptr; ASSERT_EQ( - EVP_PKEY_get_octet_string_param( - key.get(), OSSL_PKEY_PARAM_PUB_KEY, nullptr, 0, &publicKeySize - ), - 1 + EVP_PKEY_get_bn_param(key.get(), OSSL_PKEY_PARAM_EC_PUB_X, &rawX), 1 ); - crypto::Bytes publicKey(publicKeySize); ASSERT_EQ( - EVP_PKEY_get_octet_string_param( - key.get(), - OSSL_PKEY_PARAM_PUB_KEY, - publicKey.data(), - publicKey.size(), - &publicKeySize + EVP_PKEY_get_bn_param(key.get(), OSSL_PKEY_PARAM_EC_PUB_Y, &rawY), 1 + ); + std::unique_ptr x(rawX, BN_free); + std::unique_ptr y(rawY, BN_free); + crypto::Bytes publicKey(item.coordinateSize * 2 + 1); + publicKey[0] = 0x04; + ASSERT_EQ( + BN_bn2binpad( + x.get(), publicKey.data() + 1, item.coordinateSize ), - 1 + static_cast(item.coordinateSize) + ); + ASSERT_EQ( + BN_bn2binpad( + y.get(), + publicKey.data() + 1 + item.coordinateSize, + item.coordinateSize + ), + static_cast(item.coordinateSize) ); auto signature = sign(key.get(), item.digest, "message"); EXPECT_TRUE( From 5391be4b8acb57baae80b2f7e204bf95ac52ca72 Mon Sep 17 00:00:00 2001 From: dagger Date: Sat, 22 Aug 2026 17:47:59 +0700 Subject: [PATCH 04/56] update crypto docs --- doc/en/scripting/builtins/libcrypto.md | 56 ++++++++++++++------------ doc/ru/scripting/builtins/libcrypto.md | 19 ++------- 2 files changed, 33 insertions(+), 42 deletions(-) diff --git a/doc/en/scripting/builtins/libcrypto.md b/doc/en/scripting/builtins/libcrypto.md index 19b93e433..b3106d104 100644 --- a/doc/en/scripting/builtins/libcrypto.md +++ b/doc/en/scripting/builtins/libcrypto.md @@ -1,14 +1,6 @@ # *crypto* library -The library provides common cryptographic functions for mods - -TLS and HTTP are not part of this library. They can use crypto for expensive -operations - -Keys, signatures, hashes and encrypted data are passed as regular Lua strings. -Strings may contain zero bytes - -The current API version is available in `crypto.API_VERSION` +The library provides common cryptographic functions ## Hashing @@ -17,24 +9,27 @@ crypto.sha256(data: str) -> str crypto.sha384(data: str) -> str crypto.sha512(data: str) -> str crypto.md5(data: str) -> str + crypto.hash(hash: str, data: str) -> str crypto.hmac(hash: str, key: str, data: str) -> str ``` Supported names are `SHA256`, `SHA384`, `SHA512` and `MD5` -MD5 is provided for old formats. SHA256 should be used for new data +MD5 is needed for compatibility with old formats. SHA256 is better for new data -Large data can be hashed in parts +Large files can be passed in parts ```lua local hash = crypto.hash_new("SHA256") + hash:update(part1) hash:update(part2) + local result = hash:final() ``` -Call `reset` before using the context again +The context can be cleared with `reset` after `final` ## Signatures @@ -45,7 +40,11 @@ crypto.ed25519_sign(private_key: str, message: str) -> str crypto.ed25519_verify(public_key: str, message: str, signature: str) -> true -> false, error +``` +Ed25519 keys are 32 bytes long. A signature is 64 bytes long + +```lua crypto.ecdsa_keypair(curve: str) -> private_key, public_key crypto.ecdsa_public(curve: str, private_key: str) -> str crypto.ecdsa_sign(curve: str, private_key: str, message: str, hash: str) -> str @@ -53,24 +52,27 @@ crypto.ecdsa_verify(curve: str, public_key: str, message: str, signature: str, hash: str) -> true -> false, error +``` +Available curves are `P-256`, `P-384` and `P-521` + +An ECDSA public key is stored as an uncompressed SEC1 point. A signature is +stored in DER format + +```lua crypto.rsa_pkcs1_verify(hash: str, modulus: str, exponent: str, message: str, signature: str) -> true -> false, error + crypto.rsa_pss_verify(hash: str, modulus: str, exponent: str, message: str, signature: str, salt_length: int) -> true -> false, error ``` -Ed25519 keys are 32 bytes and signatures are 64 bytes - -ECDSA supports `P-256`, `P-384` and `P-521`. Public keys use the uncompressed -SEC1 format and signatures use DER - -RSA modulus and exponent use big endian. A `salt_length` of `-1` uses the hash -size. RSA key generation is not provided, Ed25519 is easier for new keys +RSA modulus and exponent are passed in big endian. A `salt_length` value of `-1` +uses the hash size ## Key exchange @@ -85,9 +87,9 @@ crypto.p256_public(private_key: str) -> str crypto.p256_shared(private_key: str, peer_public_key: str) -> str ``` -`x25519` and `x25519_shared` do the same operation +`x25519` and `x25519_shared` perform the same operation -Pass the shared secret through HKDF before using it as a key +The shared secret should not be used as a ready key. Pass it through HKDF first ## Encryption @@ -109,29 +111,31 @@ AES GCM accepts 16 and 32 byte keys ChaCha20 Poly1305 uses a 32 byte key and a 12 byte nonce -A 16 byte tag is appended to ciphertext. Decryption returns nothing when the -tag is invalid +A 16 byte tag is appended to encrypted data -Never use the same nonce twice with the same key +Never reuse a nonce with the same key ## Other functions ```lua crypto.random_bytes(length: int) -> str crypto.constant_time_equal(left: str, right: str) -> bool + crypto.hkdf_extract(hash: str, salt: str, ikm: str) -> str crypto.hkdf_expand(hash: str, prk: str, info: str, length: int) -> str + crypto.pbkdf2(hash: str, password: str, salt: str, iterations: int, length: int) -> str crypto.scrypt(password: str, salt: str, n: int, r: int, p: int, length: int, [optional]max_memory: int=0) -> str + crypto.features() -> table ``` Use scrypt or PBKDF2 with a random salt for passwords. A regular SHA256 hash is -not suitable for password storage +not suitable for passwords -`features` returns the OpenSSL version and available functions +`features` returns the OpenSSL version and a list of available features An invalid signature returns `false, "invalid_signature"` diff --git a/doc/ru/scripting/builtins/libcrypto.md b/doc/ru/scripting/builtins/libcrypto.md index dce86d401..50112710b 100644 --- a/doc/ru/scripting/builtins/libcrypto.md +++ b/doc/ru/scripting/builtins/libcrypto.md @@ -1,14 +1,6 @@ # Библиотека *crypto* -Библиотека предоставляет основные криптографические функции для модов - -TLS и HTTP в нее не входят. Эти протоколы используют crypto для тяжелых -вычислений - -Все бинарные значения передаются обычными Lua строками. Это касается ключей, -подписей, хешей и зашифрованных данных. Строки могут содержать нулевые байты - -Текущая версия API находится в `crypto.API_VERSION` +Библиотека предоставляет основные криптографические функции ## Хеширование @@ -83,9 +75,6 @@ crypto.rsa_pss_verify(hash: str, modulus: str, exponent: str, Модуль и экспонента RSA передаются в big endian. Значение `salt_length = -1` использует размер хеша -Генерации RSA ключей в библиотеке нет. Для новых ключей проще использовать -Ed25519 - ## Обмен ключами ```lua @@ -124,8 +113,7 @@ AES GCM принимает ключи размером 16 или 32 байта ChaCha20 Poly1305 использует ключ размером 32 байта и nonce размером 12 байт -В конец зашифрованных данных добавляется тег размером 16 байт. При неверном -теге функция расшифровки ничего не возвращает +В конец зашифрованных данных добавляется тег размером 16 байт Нельзя повторно использовать один nonce с тем же ключом @@ -149,8 +137,7 @@ crypto.features() -> table Для хранения паролей следует использовать scrypt или PBKDF2 со случайной солью. Обычный SHA256 для паролей не подходит -`features` возвращает версию OpenSSL и список доступных возможностей. Это -можно использовать если мод запускается на разных сборках движка +`features` возвращает версию OpenSSL и список доступных возможностей Неверная подпись возвращает `false, "invalid_signature"` From 3ed80d0c9507b034715885a5fa32de00d266900c Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 9 Sep 2026 19:26:17 +0300 Subject: [PATCH 05/56] feat: custom curves --- res/modules/animation.lua | 73 ++++++++++++++++++++-------- res/modules/internal/formats/vca.lua | 14 ++++-- 2 files changed, 64 insertions(+), 23 deletions(-) diff --git a/res/modules/animation.lua b/res/modules/animation.lua index a54a090b1..5b768a70a 100644 --- a/res/modules/animation.lua +++ b/res/modules/animation.lua @@ -9,6 +9,7 @@ local this = { INT_CONST = 1, INT_LINEAR = 2, INT_BEZIER = 3, + INT_CUSTOM = 4, TRACE_CODEGEN = false, } @@ -53,10 +54,7 @@ local function bezier_interpolation(k0, k1, t) return bezier(k0.value, k0.ry, k1.ly, k1.value, u) end -local patterns = { - {name="sint", pattern="sin(t)"}, - {name="sint2", pattern="sin(t * 2)"}, -} +local patterns = {} local exclude_patters = { "end", (string.pattern_safe("'")), @@ -127,9 +125,24 @@ local env = { local t = (frame - left.frame) / (right.frame - left.frame) if interp == INT_BEZIER then return bezier_interpolation(left, right, t) + elseif type(interp) == "function" then + return interp(left.value, right.value, t) end return left.value * (1.0 - t) + right.value * t end, + value_at_custom = function(keys, frame, func) + local left, right = key_neighbors(keys, frame) + if left == right then + return keys[left].value + end + left = keys[left] + right = keys[right] + if left == nil then + return right.value + end + local t = (frame - left.frame) / (right.frame - left.frame) + return func(left, right, t) + end, set_matrix = function(target, matrix) local info = mat4.decompose(matrix) if info then @@ -176,9 +189,18 @@ local function codegen_track(raw_track, lineset, memoised, keysets, use_tsf) keysets[lineset.target_name] = target_keysets end target_keysets[i] = line.keys - code = code .. string.format( - "\n local l%d = value_at(keysets['%s'][%d], t * %s, %s)", - i, lineset.target_name, i, raw_track.fps, line.interp) + + if line.curve_func then + local valueat = string.format("curves[%s]", string.escape(line.curve_func)) + + code = code .. string.format( + "\n local l%d = value_at_custom(keysets['%s'][%d], t * %s, %s)", + i, lineset.target_name, i, raw_track.fps, valueat) + else + code = code .. string.format( + "\n local l%d = value_at(keysets['%s'][%d], t * %s, %s)", + i, lineset.target_name, i, raw_track.fps, line.interp) + end end if line.channel == this.CH_TRANSLATE then @@ -229,7 +251,7 @@ local function codegen_track(raw_track, lineset, memoised, keysets, use_tsf) return code end -local function codegen_rig_target(raw_track, memoised, keysets) +local function codegen_rig_target(raw_track, context) local code = "\n if target.set_matrix and target.index then\n" code = code .. " local dst = DST\n" for bone, lineset in pairs(raw_track.linesets) do @@ -237,7 +259,7 @@ local function codegen_rig_target(raw_track, memoised, keysets) goto continue end local lineset_code = codegen_track( - raw_track, lineset, memoised, keysets, true) + raw_track, lineset, context.memoised, context.keysets, true) code = code .. "\n do" .. lineset_code .. "\n end\n" .. " target:set_matrix(target:index(" .. string.escape(bone) .. "), dst)\n" @@ -246,7 +268,7 @@ local function codegen_rig_target(raw_track, memoised, keysets) return code .. " end" end -local function codegen_object_target(raw_track, memoised, keysets) +local function codegen_object_target(raw_track, context) local code = "\n if target.set_pos then\n" code = code .. " local dst = DST\n" local lineset = raw_track.linesets[""] @@ -254,13 +276,13 @@ local function codegen_object_target(raw_track, memoised, keysets) return "" end local lineset_code = codegen_track( - raw_track, lineset, memoised, keysets, true) + raw_track, lineset, context.memoised, context.keysets, true) code = code .. "\n do" .. lineset_code .. "\n end\n" .. " set_matrix(target, dst)\n" return code .. " end" end -local function codegen_camera_target(raw_track, memoised, keysets) +local function codegen_camera_target(raw_track, context) local code = "\n if target.set_zoom then\n" code = code .. " local zoom = 1.0\n" local lineset = raw_track.linesets[""] @@ -268,7 +290,7 @@ local function codegen_camera_target(raw_track, memoised, keysets) return "" end local lineset_code = codegen_track( - raw_track, lineset, memoised, keysets, false) + raw_track, lineset, context.memoised, context.keysets, false) code = code .. "\n do" .. lineset_code .. "\n end\n" .. " target:set_zoom(zoom)\n" return code .. " end" @@ -276,15 +298,24 @@ end function this.compile_track(raw_track, track_name) local code = "" - local memoised = {} - local keysets = {} + local context = { + memoised = {}, + keysets = {}, + curves = {}, + } + for name, curve in pairs(raw_track.curves) do + context.curves[name] = load(string.format( + "return function(kl, kr, t) return %s end", + process_expression(curve.func, context.memoised) + ), "", "t", env)() + end - code = code .. codegen_rig_target(raw_track, memoised, keysets) - code = code .. codegen_object_target(raw_track, memoised, keysets) - code = code .. codegen_camera_target(raw_track, memoised, keysets) + code = code .. codegen_rig_target(raw_track, context) + code = code .. codegen_object_target(raw_track, context) + code = code .. codegen_camera_target(raw_track, context) local memoised_code = "" - for name, expression in pairs(memoised) do + for name, expression in pairs(context.memoised) do memoised_code = memoised_code .. "\n local " .. name .. " = " .. expression end @@ -302,7 +333,9 @@ function this.compile_track(raw_track, track_name) end local generator, err = load( - src, "", "bt", table.extend({keysets = keysets}, env)) + src, "", "bt", table.extend({ + keysets = context.keysets, curves = context.curves + }, env)) if not generator then error(err) end diff --git a/res/modules/internal/formats/vca.lua b/res/modules/internal/formats/vca.lua index 0faa89404..8146ffff6 100644 --- a/res/modules/internal/formats/vca.lua +++ b/res/modules/internal/formats/vca.lua @@ -1,7 +1,6 @@ local internals = __vc_internals local DEFAULT_FPS = 60 -local INT_BEZIER = animation.INT_BEZIER local action_to_channel = { move = animation.CH_TRANSLATE, @@ -29,14 +28,19 @@ local function parse_configure(raw_track, node) end local function parse_curve(line, node) - line.interp = curve_to_interp[node.curve] + if node.curve:starts_with(".") then + line.interp = animation.INT_CUSTOM + line.curve_func = node.curve:sub(2) + else + line.interp = curve_to_interp[node.curve] + end line.keys = {} for j, key_node in ipairs(node) do local keyframe = { frame = tonumber(key_node.frame), value = tonumber(key_node.value), } - if line.interp == INT_BEZIER then + if line.interp == animation.INT_BEZIER then keyframe.lx = tonumber(key_node.lx) keyframe.ly = tonumber(key_node.ly) keyframe.rx = tonumber(key_node.rx) @@ -51,6 +55,7 @@ local function parse_track(root) duration = math.huge, fps = DEFAULT_FPS, linesets = {}, + curves = {}, } local linesets = raw_track.linesets for i, node in ipairs(root) do @@ -61,6 +66,9 @@ local function parse_track(root) if tag == "configure" then parse_configure(raw_track, node) goto continue + elseif tag == "curve" then + raw_track.curves[node.name] = node + goto continue end local target_type = nil From 9a87c2082362f24c36114b1cf57626fc5ed70b2e Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 9 Sep 2026 23:43:24 +0300 Subject: [PATCH 06/56] add std::string util::lower_case, upper_case overloads --- src/util/stringutil.cpp | 8 ++++++++ src/util/stringutil.hpp | 2 ++ 2 files changed, 10 insertions(+) diff --git a/src/util/stringutil.cpp b/src/util/stringutil.cpp index 55529a398..8d80a82ab 100644 --- a/src/util/stringutil.cpp +++ b/src/util/stringutil.cpp @@ -543,6 +543,14 @@ static std::basic_string upper_case(const std::basic_string& str) return result; } +std::string util::lower_case(const std::string& str) { + return ::lower_case(str); +} + +std::string util::upper_case(const std::string& str) { + return ::upper_case(str); +} + std::wstring util::lower_case(const std::wstring& str) { return ::lower_case(str); } diff --git a/src/util/stringutil.hpp b/src/util/stringutil.hpp index c26f02741..3f2263efb 100644 --- a/src/util/stringutil.hpp +++ b/src/util/stringutil.hpp @@ -101,6 +101,8 @@ namespace util { std::string capitalized(const std::string& str); + std::string lower_case(const std::string& str); + std::string upper_case(const std::string& str); std::wstring lower_case(const std::wstring& str); std::wstring upper_case(const std::wstring& str); std::wstring capitalized(const std::wstring& str); From 74bb695807ea0a0816ee8ad3da4addd56b26d2cf Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 9 Sep 2026 23:43:44 +0300 Subject: [PATCH 07/56] cleanup http requests (part 1) --- src/logic/scripting/lua/libs/libnetwork.cpp | 32 ++++---- src/network/Curl.cpp | 85 +++++---------------- src/network/Network.cpp | 23 +----- src/network/Network.hpp | 17 +---- src/network/commons.hpp | 33 ++++---- test/network/curltest.cpp | 26 ++++--- 6 files changed, 73 insertions(+), 143 deletions(-) diff --git a/src/logic/scripting/lua/libs/libnetwork.cpp b/src/logic/scripting/lua/libs/libnetwork.cpp index 55db6e550..1493eb0f0 100644 --- a/src/logic/scripting/lua/libs/libnetwork.cpp +++ b/src/logic/scripting/lua/libs/libnetwork.cpp @@ -3,6 +3,7 @@ #include "engine/Engine.hpp" #include "network/Network.hpp" #include "devtools/Project.hpp" +#include "util/stringutil.hpp" #include #include @@ -91,24 +92,26 @@ static int perform_get(lua::State* L, network::Network& network, bool binary) { int currentRequestId = request_id++; - network.get( - url, + network::HttpRequest request {}; + request.url = std::move(url); + request.onResponse = [currentRequestId, binary](std::vector bytes) { push_event(NetworkEvent( RESPONSE, ResponseEventDto { 200, binary, currentRequestId, std::move(bytes)} )); - }, + }; + request.onReject = [currentRequestId, binary](int code, std::vector bytes) { push_event(NetworkEvent( RESPONSE, ResponseEventDto { code, binary, currentRequestId, std::move(bytes)} )); - }, - std::move(headers) - ); + }; + request.headers = std::move(headers); + network.request(std::move(request)); return lua::pushinteger(L, currentRequestId); } @@ -134,25 +137,28 @@ static int l_post(lua::State* L, network::Network& network) { auto headers = read_headers(L, 3); int currentRequestId = request_id++; - network.post( - url, - string, + network::HttpRequest request {}; + request.url = std::move(url); + request.body = string; + request.headers = std::move(headers); + request.headers.emplace_back("Content-Type: application/json"); + request.onResponse = [currentRequestId](std::vector bytes) { push_event(NetworkEvent( RESPONSE, ResponseEventDto { 200, false, currentRequestId, std::move(bytes)} )); - }, + }; + request.onReject = [currentRequestId](int code, std::vector bytes) { push_event(NetworkEvent( RESPONSE, ResponseEventDto { code, false, currentRequestId, std::move(bytes)} )); - }, - std::move(headers) - ); + }; + network.request(std::move(request)); return lua::pushinteger(L, currentRequestId); } diff --git a/src/network/Curl.cpp b/src/network/Curl.cpp index 1caafde73..4aad9c612 100644 --- a/src/network/Curl.cpp +++ b/src/network/Curl.cpp @@ -23,21 +23,6 @@ static size_t write_callback( return size * nmemb; } -enum class RequestType { - GET, POST -}; - -struct Request { - RequestType type; - std::string url; - OnResponse onResponse; - OnReject onReject; - long maxSize; - bool followLocation = false; - std::string data; - std::vector headers; -}; - class CurlRequests : public Requests { CURLM* multiHandle; CURL* curl; @@ -50,7 +35,7 @@ class CurlRequests : public Requests { std::vector buffer; std::string url; - std::queue requests; + std::queue requests; public: CurlRequests(CURLM* multiHandle, CURL* curl) : multiHandle(multiHandle), curl(curl) { @@ -61,47 +46,12 @@ public: curl_easy_cleanup(curl); curl_multi_cleanup(multiHandle); } - void get( - const std::string& url, - OnResponse onResponse, - OnReject onReject, - std::vector headers, - long maxSize - ) override { - Request request { - RequestType::GET, - url, - onResponse, - onReject, - maxSize, - true, - "", - std::move(headers)}; + + void request(HttpRequest request) override { processRequest(std::move(request)); } - void post( - const std::string& url, - const std::string& data, - OnResponse onResponse, - OnReject onReject=nullptr, - std::vector headers = {}, - long maxSize=0 - ) override { - Request request { - RequestType::POST, - url, - onResponse, - onReject, - maxSize, - false, - "", - std::move(headers)}; - request.data = data; - processRequest(std::move(request)); - } - - void processRequest(Request request) { + void processRequest(HttpRequest request) { if (!url.empty()) { requests.push(request); return; @@ -113,7 +63,19 @@ public: buffer.clear(); curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); - curl_easy_setopt(curl, CURLOPT_POST, request.type == RequestType::POST); + switch (request.method) { + case HttpMethod::GET: + break; + case HttpMethod::POST: + curl_easy_setopt(curl, CURLOPT_POST, true); + break; + case HttpMethod::PUT: + curl_easy_setopt(curl, CURLOPT_UPLOAD, true); + break; + case HttpMethod::DELETE: + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE"); + break; + } curl_slist* hs = nullptr; @@ -121,16 +83,9 @@ public: hs = curl_slist_append(hs, header.c_str()); } - switch (request.type) { - case RequestType::GET: - break; - case RequestType::POST: - hs = curl_slist_append(hs, "Content-Type: application/json"); - curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, request.data.length()); - curl_easy_setopt(curl, CURLOPT_COPYPOSTFIELDS, request.data.c_str()); - break; - default: - throw std::runtime_error("not implemented"); + if (!request.body.empty()) { + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, request.body.length()); + curl_easy_setopt(curl, CURLOPT_COPYPOSTFIELDS, request.body.data()); } curl_easy_setopt(curl, CURLOPT_HTTPHEADER, hs); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, request.followLocation); diff --git a/src/network/Network.cpp b/src/network/Network.cpp index a0e1eb606..7b526a29a 100644 --- a/src/network/Network.cpp +++ b/src/network/Network.cpp @@ -51,27 +51,8 @@ Network::Network(std::unique_ptr requests) Network::~Network() = default; -void Network::get( - const std::string& url, - OnResponse onResponse, - OnReject onReject, - std::vector headers, - long maxSize -) { - requests->get(url, onResponse, onReject, std::move(headers), maxSize); -} - -void Network::post( - const std::string& url, - const std::string& fieldsData, - OnResponse onResponse, - OnReject onReject, - std::vector headers, - long maxSize -) { - requests->post( - url, fieldsData, onResponse, onReject, std::move(headers), maxSize - ); +void Network::request(HttpRequest request) { + requests->request(std::move(request)); } Connection* Network::getConnection(u64id_t id, bool includePrivate) { diff --git a/src/network/Network.hpp b/src/network/Network.hpp index 6d5cd6d54..49f44af6c 100644 --- a/src/network/Network.hpp +++ b/src/network/Network.hpp @@ -68,22 +68,7 @@ namespace network { Network(std::unique_ptr requests); ~Network(); - void get( - const std::string& url, - OnResponse onResponse, - OnReject onReject = nullptr, - std::vector headers = {}, - long maxSize=0 - ); - - void post( - const std::string& url, - const std::string& fieldsData, - OnResponse onResponse, - OnReject onReject = nullptr, - std::vector headers = {}, - long maxSize=0 - ); + void request(HttpRequest request); [[nodiscard]] Connection* getConnection(u64id_t id, bool includePrivate); [[nodiscard]] Server* getServer(u64id_t id, bool includePrivate) const; diff --git a/src/network/commons.hpp b/src/network/commons.hpp index 8b834bb3e..893a88ebe 100644 --- a/src/network/commons.hpp +++ b/src/network/commons.hpp @@ -10,6 +10,10 @@ #include namespace network { + enum class HttpMethod { + GET, POST, PUT, DELETE, + }; + using OnResponse = std::function)>; using OnReject = std::function)>; using ConnectCallback = std::function; @@ -17,26 +21,23 @@ namespace network { using ServerDatagramCallback = std::function; using ClientDatagramCallback = std::function; + struct HttpRequest { + HttpMethod method; + std::string url; + std::string body; + std::vector headers; + + OnResponse onResponse; + OnReject onReject; + bool followLocation = false; + long maxSize = -1; + }; + class Requests { public: virtual ~Requests() {} - virtual void get( - const std::string& url, - OnResponse onResponse, - OnReject onReject=nullptr, - std::vector headers = {}, - long maxSize=0 - ) = 0; - - virtual void post( - const std::string& url, - const std::string& data, - OnResponse onResponse, - OnReject onReject=nullptr, - std::vector headers = {}, - long maxSize=0 - ) = 0; + virtual void request(HttpRequest request) = 0; [[nodiscard]] virtual size_t getTotalUpload() const = 0; [[nodiscard]] virtual size_t getTotalDownload() const = 0; diff --git a/test/network/curltest.cpp b/test/network/curltest.cpp index 42806c77c..44f87d17e 100644 --- a/test/network/curltest.cpp +++ b/test/network/curltest.cpp @@ -6,18 +6,20 @@ TEST(curltest, curltest) { NetworkSettings settings {}; auto network = network::Network::create(settings); - network->get( - "https://raw.githubusercontent.com/MihailRis/VoxelEngine-Cpp/refs/" - "heads/curl/res/content/base/blocks/lamp.json", - [](std::vector data) { - if (data.empty()) { - return; - } - auto view = std::string_view(data.data(), data.size()); - auto value = json::parse(view); - std::cout << value << std::endl; - }, [](auto, auto){} - ); + + network::HttpRequest request {}; + request.url = "https://raw.githubusercontent.com/MihailRis/VoxelEngine-Cpp/refs/" + "heads/curl/res/content/base/blocks/lamp.json"; + request.onResponse = [](std::vector data) { + if (data.empty()) { + return; + } + auto view = std::string_view(data.data(), data.size()); + auto value = json::parse(view); + std::cout << value << std::endl; + }; + network->request(std::move(request)); + std::cout << "upload: " << network->getTotalUpload() << " B" << std::endl; std::cout << "download: " << network->getTotalDownload() << " B" << std::endl; } From 018e4c9f2c5e480c13103b6d2b2f8eeb508244e3 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 9 Sep 2026 23:47:23 +0300 Subject: [PATCH 08/56] remove HttpMethod enum --- src/network/Curl.cpp | 14 +------------- src/network/commons.hpp | 6 +----- 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/src/network/Curl.cpp b/src/network/Curl.cpp index 4aad9c612..6e89418c9 100644 --- a/src/network/Curl.cpp +++ b/src/network/Curl.cpp @@ -63,19 +63,7 @@ public: buffer.clear(); curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); - switch (request.method) { - case HttpMethod::GET: - break; - case HttpMethod::POST: - curl_easy_setopt(curl, CURLOPT_POST, true); - break; - case HttpMethod::PUT: - curl_easy_setopt(curl, CURLOPT_UPLOAD, true); - break; - case HttpMethod::DELETE: - curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE"); - break; - } + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, request.method); curl_slist* hs = nullptr; diff --git a/src/network/commons.hpp b/src/network/commons.hpp index 893a88ebe..614821404 100644 --- a/src/network/commons.hpp +++ b/src/network/commons.hpp @@ -10,10 +10,6 @@ #include namespace network { - enum class HttpMethod { - GET, POST, PUT, DELETE, - }; - using OnResponse = std::function)>; using OnReject = std::function)>; using ConnectCallback = std::function; @@ -22,7 +18,7 @@ namespace network { using ClientDatagramCallback = std::function; struct HttpRequest { - HttpMethod method; + std::string method; std::string url; std::string body; std::vector headers; From 1c7bf2de01f46a70dbaaa9d85d8e6eaa2e7b5a8d Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 9 Sep 2026 23:53:03 +0300 Subject: [PATCH 09/56] add network.request --- res/scripts/classes.lua | 27 ++++++++++++++++--- src/logic/scripting/lua/libs/libnetwork.cpp | 30 +++++++++++++++++++++ 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/res/scripts/classes.lua b/res/scripts/classes.lua index 280c5ddeb..008d51d2f 100644 --- a/res/scripts/classes.lua +++ b/res/scripts/classes.lua @@ -100,8 +100,29 @@ local _udp_client_open_callbacks = {} local _http_response_callbacks = {} local _http_error_callbacks = {} +local http_request = network.__request +local http_get = network.__get +local http_get_binary = network.__get_binary +local http_post = network.__post +network.__request = nil +network.__get = nil +network.__get_binary = nil +network.__post = nil + +local function request(url, params) + local id = http_request(url, params) + if params.on_response then + _http_response_callbacks[id] = params.on_response + end + if params.on_error then + _http_error_callbacks[id] = params.on_error + end +end + +network.request = request + network.get = function(url, callback, errorCallback, headers) - local id = network.__get(url, headers) + local id = http_get(url, headers) if callback then _http_response_callbacks[id] = callback end @@ -111,7 +132,7 @@ network.get = function(url, callback, errorCallback, headers) end network.get_binary = function(url, callback, errorCallback, headers) - local id = network.__get_binary(url, headers) + local id = http_get_binary(url, headers) if callback then _http_response_callbacks[id] = callback end @@ -121,7 +142,7 @@ network.get_binary = function(url, callback, errorCallback, headers) end network.post = function(url, data, callback, errorCallback, headers) - local id = network.__post(url, data, headers) + local id = http_post(url, data, headers) if callback then _http_response_callbacks[id] = callback end diff --git a/src/logic/scripting/lua/libs/libnetwork.cpp b/src/logic/scripting/lua/libs/libnetwork.cpp index 1493eb0f0..318dfcc7c 100644 --- a/src/logic/scripting/lua/libs/libnetwork.cpp +++ b/src/logic/scripting/lua/libs/libnetwork.cpp @@ -86,6 +86,35 @@ static std::vector read_headers(lua::State* L, int index) { static int request_id = 1; +static int l_request(lua::State* L, network::Network& network) { + network::HttpRequest request {}; + request.url = lua::require_lstring(L, 1); + + if (!lua::istable(L, 2)) { + throw std::runtime_error("table expected as argument #2"); + } + if (lua::getfield(L, "method", 2)) { + request.method = util::upper_case(lua::require_string(L, -1)); + lua::pop(L); + } + if (lua::getfield(L, "headers", 2)) { + request.headers = read_headers(L, -1); + lua::pop(L); + } + if (lua::getfield(L, "body", 2)) { + request.body = lua::require_lstring(L, -1); + lua::pop(L); + } + if (lua::getfield(L, "follow_location", 2)) { + request.followLocation = lua::toboolean(L, -1); + lua::pop(L); + } + + int currentRequestId = request_id++; + network.request(std::move(request)); + return lua::pushinteger(L, currentRequestId); +} + static int perform_get(lua::State* L, network::Network& network, bool binary) { std::string url(lua::require_lstring(L, 1)); auto headers = read_headers(L, 2); @@ -568,6 +597,7 @@ int wrap(lua_State* L) { } const luaL_Reg networklib[] = { + {"__request", wrap}, {"__get", wrap}, {"__get_binary", wrap}, {"__post", wrap}, From 894101a46aca6f958d5cdeeddc8bf6e741462676 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 9 Sep 2026 23:57:58 +0300 Subject: [PATCH 10/56] cleanup network library --- res/scripts/classes.lua | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/res/scripts/classes.lua b/res/scripts/classes.lua index 008d51d2f..c6e25b0b9 100644 --- a/res/scripts/classes.lua +++ b/res/scripts/classes.lua @@ -104,10 +104,18 @@ local http_request = network.__request local http_get = network.__get local http_get_binary = network.__get_binary local http_post = network.__post +local open_tcp = network.__open_tcp +local open_udp = network.__open_udp +local connect_tcp = network.__connect_tcp +local connect_udp = network.__connect_udp network.__request = nil network.__get = nil network.__get_binary = nil network.__post = nil +network.__open_tcp = nil +network.__open_udp = nil +network.__connect_tcp = nil +network.__connect_udp = nil local function request(url, params) local id = http_request(url, params) @@ -152,7 +160,7 @@ network.post = function(url, data, callback, errorCallback, headers) end network.tcp_open = function (port, handler) - local socket = setmetatable({id=network.__open_tcp(port)}, ServerSocket) + local socket = setmetatable({id=open_tcp(port)}, ServerSocket) _tcp_server_callbacks[socket.id] = function(id) handler(setmetatable({id=id}, Socket)) @@ -162,7 +170,7 @@ end network.tcp_connect = function(address, port, callback, errorCallback) local socket = setmetatable({id=0}, Socket) - socket.id = network.__connect_tcp(address, port) + socket.id = connect_tcp(address, port) _tcp_client_callbacks[socket.id] = function() callback(socket) end if errorCallback then _tcp_client_error_callbacks[socket.id] = function(message) errorCallback(socket, message) end @@ -175,7 +183,7 @@ network.udp_open = function (port, datagramHandler) error "udp server cannot be opened without datagram handler" end - local socket = setmetatable({id=network.__open_udp(port)}, DatagramServerSocket) + local socket = setmetatable({id=open_udp(port)}, DatagramServerSocket) _udp_server_callbacks[socket.id] = function(address, port, data) datagramHandler(address, port, data, socket) @@ -190,7 +198,7 @@ network.udp_connect = function (address, port, datagramHandler, openCallback) end local socket = setmetatable({id=0}, WriteableSocket) - socket.id = network.__connect_udp(address, port) + socket.id = connect_udp(address, port) _udp_client_datagram_callbacks[socket.id] = datagramHandler if openCallback then From 03efd8c6696dd3a299bcf0555bf8c6e9b2903850 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 10 Sep 2026 00:01:03 +0300 Subject: [PATCH 11/56] fix --- src/network/Curl.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/network/Curl.cpp b/src/network/Curl.cpp index 6e89418c9..872e6d680 100644 --- a/src/network/Curl.cpp +++ b/src/network/Curl.cpp @@ -63,7 +63,7 @@ public: buffer.clear(); curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); - curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, request.method); + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, request.method.c_str()); curl_slist* hs = nullptr; From 48ca3ab5f9dc8f3e3ab7dcd08c3c8b7360fdcf36 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 10 Sep 2026 00:11:04 +0300 Subject: [PATCH 12/56] move animation code generation to separate module --- res/modules/animation.lua | 316 +-------------------- res/modules/internal/animation_codegen.lua | 298 +++++++++++++++++++ res/modules/internal/formats/vca.lua | 2 +- res/scripts/stdlib.lua | 1 + 4 files changed, 313 insertions(+), 304 deletions(-) create mode 100644 res/modules/internal/animation_codegen.lua diff --git a/res/modules/animation.lua b/res/modules/animation.lua index 5b768a70a..c42b15d25 100644 --- a/res/modules/animation.lua +++ b/res/modules/animation.lua @@ -1,22 +1,5 @@ local internals = __vc_internals -local this = { - CH_TRANSLATE = 1, - CH_ROTATE = 2, - CH_SCALE = 3, - CH_ZOOM = 4, - - INT_CONST = 1, - INT_LINEAR = 2, - INT_BEZIER = 3, - INT_CUSTOM = 4, - - TRACE_CODEGEN = false, -} - -local INT_CONST = this.INT_CONST -local INT_BEZIER = this.INT_BEZIER - local function bezier(a, b, c, d, u) local s = 1 - u return s * s * s * a + @@ -54,296 +37,23 @@ local function bezier_interpolation(k0, k1, t) return bezier(k0.value, k0.ry, k1.ly, k1.value, u) end -local patterns = {} -local exclude_patters = { - "end", - (string.pattern_safe("'")), - (string.pattern_safe('"')), - (string.pattern_safe("--")), - (string.pattern_safe("..")), -} +local this = { + CH_TRANSLATE = 1, + CH_ROTATE = 2, + CH_SCALE = 3, + CH_ZOOM = 4, --- TODO: replace with actual expression -> lua translator -local function process_expression(src, memoised) - for i, pattern in ipairs(exclude_patters) do - if src:find(pattern) then - debug.print(exclude_patters) - error("invalid syntax "..string.escape(src)) - end - end - for i, pattern in ipairs(patterns) do - local pattern_safe = string.pattern_safe(pattern.pattern) - if src:find(pattern_safe) then - memoised[pattern.name] = pattern.pattern - src = src:gsub(pattern_safe, pattern.name) - end - end - return src -end + INT_CONST = 1, + INT_LINEAR = 2, + INT_BEZIER = 3, + INT_CUSTOM = 4, -local function key_neighbors(keys, frame) - local left = 1 - local right = #keys + TRACE_CODEGEN = false, - while left <= right do - local mid = math.floor((left + right) / 2) - - if keys[mid].frame < frame then - left = mid + 1 - elseif keys[mid].frame > frame then - right = mid - 1 - else - return mid, mid - end - end - if left > #keys then - left = #keys - end - return right, left -end - -local env = { - mat4 = mat4, - e = math.exp(1), - X = {1, 0, 0}, - Y = {0, 1, 0}, - Z = {0, 0, 1}, - DST = mat4.idt(), - value_at = function(keys, frame, interp) - local left, right = key_neighbors(keys, frame) - if left == right then - return keys[left].value - end - left = keys[left] - if interp == INT_CONST then - return left.value - end - right = keys[right] - if left == nil then - return right.value - end - local t = (frame - left.frame) / (right.frame - left.frame) - if interp == INT_BEZIER then - return bezier_interpolation(left, right, t) - elseif type(interp) == "function" then - return interp(left.value, right.value, t) - end - return left.value * (1.0 - t) + right.value * t - end, - value_at_custom = function(keys, frame, func) - local left, right = key_neighbors(keys, frame) - if left == right then - return keys[left].value - end - left = keys[left] - right = keys[right] - if left == nil then - return right.value - end - local t = (frame - left.frame) / (right.frame - left.frame) - return func(left, right, t) - end, - set_matrix = function(target, matrix) - local info = mat4.decompose(matrix) - if info then - local pos = info.translation - local rot = info.rotation - local scale = info.scale - if target.set_pos then - target:set_pos(pos) - end - if target.set_rot then - target:set_rot(rot) - end - if target.set_scale then - target:set_scale(scale) - end - end - end, - dump = debug.print -} - -local math_funcs = { - "sqrt", "min", "max", "deg", "rad", "log", "log10", "floor", "ceil", "sin", - "tan", "noise", "noise2", "sign", "round", "exp", "pi", "e" -} -for _, name in ipairs(math_funcs) do - env[name] = math[name] -end - -local function codegen_track(raw_track, lineset, memoised, keysets, use_tsf) - local lines = lineset.lines - local code = "" - local has_tsf = false - local translation = {false, false, false} - local rotation = {false, false, false} - local scale = {false, false, false} - for i, line in ipairs(lines) do - if line.expression then - code = code .. "\n local l" .. i .. " = (" .. - process_expression(line.expression, memoised) .. ")" - elseif line.keys then - local target_keysets = keysets[lineset.target_name] - if not target_keysets then - target_keysets = {} - keysets[lineset.target_name] = target_keysets - end - target_keysets[i] = line.keys - - if line.curve_func then - local valueat = string.format("curves[%s]", string.escape(line.curve_func)) - - code = code .. string.format( - "\n local l%d = value_at_custom(keysets['%s'][%d], t * %s, %s)", - i, lineset.target_name, i, raw_track.fps, valueat) - else - code = code .. string.format( - "\n local l%d = value_at(keysets['%s'][%d], t * %s, %s)", - i, lineset.target_name, i, raw_track.fps, line.interp) - end - end - - if line.channel == this.CH_TRANSLATE then - translation[line.axis] = i - has_tsf = true - elseif line.channel == this.CH_ROTATE then - rotation[line.axis] = i - has_tsf = true - elseif line.channel == this.CH_SCALE then - scale[line.axis] = i - has_tsf = true - elseif line.channel == this.CH_ZOOM then - code = code .. "\n zoom = l" .. i - end - end - - if not has_tsf or not use_tsf then - return code - end - - code = code .. "\n mat4.idt(dst)" - if translation[1] or translation[2] or translation[3] then - code = code .. "\n mat4.translate(dst, {" .. - (translation[1] and ("l" .. translation[1]) or '0').. ", " .. - (translation[2] and ("l" .. translation[2]) or '0').. ", " .. - (translation[3] and ("l" .. translation[3]) or '0').. "}, dst)" - end - - local axis_names = {"X", "Y", "Z"} - local axis_indices = {X=1, Y=2, Z=3} - local rotation_order = raw_track.rotation_order - for i=1,3 do - local axis = axis_indices[rotation_order[i]] - local var = rotation[axis] - if var then - code = code .. "\n mat4.rotate(dst, " .. axis_names[axis] .. - ", l" .. var .. ", dst)" - end - end - - if scale[1] or scale[2] or scale[3] then - code = code .. "\n mat4.scale(dst, {" .. - (scale[1] and ("l" .. scale[1]) or '1').. ", " .. - (scale[2] and ("l" .. scale[2]) or '1').. ", " .. - (scale[3] and ("l" .. scale[3]) or '1').. "}, dst)" - end - - return code -end - -local function codegen_rig_target(raw_track, context) - local code = "\n if target.set_matrix and target.index then\n" - code = code .. " local dst = DST\n" - for bone, lineset in pairs(raw_track.linesets) do - if lineset.target_type ~= "bone" then - goto continue - end - local lineset_code = codegen_track( - raw_track, lineset, context.memoised, context.keysets, true) - - code = code .. "\n do" .. lineset_code .. "\n end\n" .. - " target:set_matrix(target:index(" .. string.escape(bone) .. "), dst)\n" - ::continue:: - end - return code .. " end" -end - -local function codegen_object_target(raw_track, context) - local code = "\n if target.set_pos then\n" - code = code .. " local dst = DST\n" - local lineset = raw_track.linesets[""] - if not lineset then - return "" - end - local lineset_code = codegen_track( - raw_track, lineset, context.memoised, context.keysets, true) - code = code .. "\n do" .. lineset_code .. "\n end\n" - .. " set_matrix(target, dst)\n" - return code .. " end" -end - -local function codegen_camera_target(raw_track, context) - local code = "\n if target.set_zoom then\n" - code = code .. " local zoom = 1.0\n" - local lineset = raw_track.linesets[""] - if not lineset then - return "" - end - local lineset_code = codegen_track( - raw_track, lineset, context.memoised, context.keysets, false) - code = code .. "\n do" .. lineset_code .. "\n end\n" - .. " target:set_zoom(zoom)\n" - return code .. " end" -end - -function this.compile_track(raw_track, track_name) - local code = "" - local context = { - memoised = {}, - keysets = {}, - curves = {}, + maths = { + bezier_interpolation = bezier_interpolation } - for name, curve in pairs(raw_track.curves) do - context.curves[name] = load(string.format( - "return function(kl, kr, t) return %s end", - process_expression(curve.func, context.memoised) - ), "", "t", env)() - end - - code = code .. codegen_rig_target(raw_track, context) - code = code .. codegen_object_target(raw_track, context) - code = code .. codegen_camera_target(raw_track, context) - - local memoised_code = "" - for name, expression in pairs(context.memoised) do - memoised_code = memoised_code .. "\n local " .. name .. " = " - .. expression - end - - if #memoised_code > 0 then - code = memoised_code .. "\n" .. code - end - - local src = "return function(target, t, m)\n" - .. code .. "\nend" - - if this.TRACE_CODEGEN then - debug.log("[".. - string.escape(track_name or "nil").." codegen trace]:\n"..src) - end - - local generator, err = load( - src, "", "bt", table.extend({ - keysets = context.keysets, curves = context.curves - }, env)) - if not generator then - error(err) - end - return { - duration = raw_track.duration, - func = generator(), - } -end +} local loaded_tracks = {} local backup_tracks = {} diff --git a/res/modules/internal/animation_codegen.lua b/res/modules/internal/animation_codegen.lua new file mode 100644 index 000000000..3301f401f --- /dev/null +++ b/res/modules/internal/animation_codegen.lua @@ -0,0 +1,298 @@ +local internals = __vc_internals + +local INT_CONST = animation.INT_CONST +local INT_BEZIER = animation.INT_BEZIER + +local bezier_interpolation = animation.maths.bezier_interpolation + +local patterns = {} +local exclude_patters = { + "end", + (string.pattern_safe("'")), + (string.pattern_safe('"')), + (string.pattern_safe("--")), + (string.pattern_safe("..")), +} + +-- TODO: replace with actual expression -> lua translator +local function process_expression(src, memoised) + for i, pattern in ipairs(exclude_patters) do + if src:find(pattern) then + debug.print(exclude_patters) + error("invalid syntax "..string.escape(src)) + end + end + for i, pattern in ipairs(patterns) do + local pattern_safe = string.pattern_safe(pattern.pattern) + if src:find(pattern_safe) then + memoised[pattern.name] = pattern.pattern + src = src:gsub(pattern_safe, pattern.name) + end + end + return src +end + +local function key_neighbors(keys, frame) + local left = 1 + local right = #keys + + while left <= right do + local mid = math.floor((left + right) / 2) + + if keys[mid].frame < frame then + left = mid + 1 + elseif keys[mid].frame > frame then + right = mid - 1 + else + return mid, mid + end + end + if left > #keys then + left = #keys + end + return right, left +end + +local env = { + mat4 = mat4, + e = math.exp(1), + X = {1, 0, 0}, + Y = {0, 1, 0}, + Z = {0, 0, 1}, + DST = mat4.idt(), + value_at = function(keys, frame, interp) + local left, right = key_neighbors(keys, frame) + if left == right then + return keys[left].value + end + left = keys[left] + if interp == INT_CONST then + return left.value + end + right = keys[right] + if left == nil then + return right.value + end + local t = (frame - left.frame) / (right.frame - left.frame) + if interp == INT_BEZIER then + return bezier_interpolation(left, right, t) + elseif type(interp) == "function" then + return interp(left.value, right.value, t) + end + return left.value * (1.0 - t) + right.value * t + end, + value_at_custom = function(keys, frame, func) + local left, right = key_neighbors(keys, frame) + if left == right then + return keys[left].value + end + left = keys[left] + right = keys[right] + if left == nil then + return right.value + end + local t = (frame - left.frame) / (right.frame - left.frame) + return func(left, right, t) + end, + set_matrix = function(target, matrix) + local info = mat4.decompose(matrix) + if info then + local pos = info.translation + local rot = info.rotation + local scale = info.scale + if target.set_pos then + target:set_pos(pos) + end + if target.set_rot then + target:set_rot(rot) + end + if target.set_scale then + target:set_scale(scale) + end + end + end, + dump = debug.print +} + +local math_funcs = { + "sqrt", "min", "max", "deg", "rad", "log", "log10", "floor", "ceil", "sin", + "tan", "noise", "noise2", "sign", "round", "exp", "pi", "e" +} +for _, name in ipairs(math_funcs) do + env[name] = math[name] +end + +local function codegen_track(raw_track, lineset, memoised, keysets, use_tsf) + local lines = lineset.lines + local code = "" + local has_tsf = false + local translation = {false, false, false} + local rotation = {false, false, false} + local scale = {false, false, false} + for i, line in ipairs(lines) do + if line.expression then + code = code .. "\n local l" .. i .. " = (" .. + process_expression(line.expression, memoised) .. ")" + elseif line.keys then + local target_keysets = keysets[lineset.target_name] + if not target_keysets then + target_keysets = {} + keysets[lineset.target_name] = target_keysets + end + target_keysets[i] = line.keys + + if line.curve_func then + local valueat = string.format("curves[%s]", string.escape(line.curve_func)) + + code = code .. string.format( + "\n local l%d = value_at_custom(keysets['%s'][%d], t * %s, %s)", + i, lineset.target_name, i, raw_track.fps, valueat) + else + code = code .. string.format( + "\n local l%d = value_at(keysets['%s'][%d], t * %s, %s)", + i, lineset.target_name, i, raw_track.fps, line.interp) + end + end + + if line.channel == animation.CH_TRANSLATE then + translation[line.axis] = i + has_tsf = true + elseif line.channel == animation.CH_ROTATE then + rotation[line.axis] = i + has_tsf = true + elseif line.channel == animation.CH_SCALE then + scale[line.axis] = i + has_tsf = true + elseif line.channel == animation.CH_ZOOM then + code = code .. "\n zoom = l" .. i + end + end + + if not has_tsf or not use_tsf then + return code + end + + code = code .. "\n mat4.idt(dst)" + if translation[1] or translation[2] or translation[3] then + code = code .. "\n mat4.translate(dst, {" .. + (translation[1] and ("l" .. translation[1]) or '0').. ", " .. + (translation[2] and ("l" .. translation[2]) or '0').. ", " .. + (translation[3] and ("l" .. translation[3]) or '0').. "}, dst)" + end + + local axis_names = {"X", "Y", "Z"} + local axis_indices = {X=1, Y=2, Z=3} + local rotation_order = raw_track.rotation_order + for i=1,3 do + local axis = axis_indices[rotation_order[i]] + local var = rotation[axis] + if var then + code = code .. "\n mat4.rotate(dst, " .. axis_names[axis] .. + ", l" .. var .. ", dst)" + end + end + + if scale[1] or scale[2] or scale[3] then + code = code .. "\n mat4.scale(dst, {" .. + (scale[1] and ("l" .. scale[1]) or '1').. ", " .. + (scale[2] and ("l" .. scale[2]) or '1').. ", " .. + (scale[3] and ("l" .. scale[3]) or '1').. "}, dst)" + end + + return code +end + +local function codegen_rig_target(raw_track, context) + local code = "\n if target.set_matrix and target.index then\n" + code = code .. " local dst = DST\n" + for bone, lineset in pairs(raw_track.linesets) do + if lineset.target_type ~= "bone" then + goto continue + end + local lineset_code = codegen_track( + raw_track, lineset, context.memoised, context.keysets, true) + + code = code .. "\n do" .. lineset_code .. "\n end\n" .. + " target:set_matrix(target:index(" .. string.escape(bone) .. "), dst)\n" + ::continue:: + end + return code .. " end" +end + +local function codegen_object_target(raw_track, context) + local code = "\n if target.set_pos then\n" + code = code .. " local dst = DST\n" + local lineset = raw_track.linesets[""] + if not lineset then + return "" + end + local lineset_code = codegen_track( + raw_track, lineset, context.memoised, context.keysets, true) + code = code .. "\n do" .. lineset_code .. "\n end\n" + .. " set_matrix(target, dst)\n" + return code .. " end" +end + +local function codegen_camera_target(raw_track, context) + local code = "\n if target.set_zoom then\n" + code = code .. " local zoom = 1.0\n" + local lineset = raw_track.linesets[""] + if not lineset then + return "" + end + local lineset_code = codegen_track( + raw_track, lineset, context.memoised, context.keysets, false) + code = code .. "\n do" .. lineset_code .. "\n end\n" + .. " target:set_zoom(zoom)\n" + return code .. " end" +end + + +function internals.compile_animation_track(raw_track, track_name) + local code = "" + local context = { + memoised = {}, + keysets = {}, + curves = {}, + } + for name, curve in pairs(raw_track.curves) do + context.curves[name] = load(string.format( + "return function(kl, kr, t) return %s end", + process_expression(curve.func, context.memoised) + ), "", "t", env)() + end + + code = code .. codegen_rig_target(raw_track, context) + code = code .. codegen_object_target(raw_track, context) + code = code .. codegen_camera_target(raw_track, context) + + local memoised_code = "" + for name, expression in pairs(context.memoised) do + memoised_code = memoised_code .. "\n local " .. name .. " = " + .. expression + end + + if #memoised_code > 0 then + code = memoised_code .. "\n" .. code + end + + local src = "return function(target, t, m)\n" + .. code .. "\nend" + + if animation.TRACE_CODEGEN then + debug.log("[".. + string.escape(track_name or "nil").." codegen trace]:\n"..src) + end + + local generator, err = load( + src, "", "bt", table.extend({ + keysets = context.keysets, curves = context.curves + }, env)) + if not generator then + error(err) + end + return { + duration = raw_track.duration, + func = generator(), + } +end diff --git a/res/modules/internal/formats/vca.lua b/res/modules/internal/formats/vca.lua index 8146ffff6..051066ecd 100644 --- a/res/modules/internal/formats/vca.lua +++ b/res/modules/internal/formats/vca.lua @@ -106,7 +106,7 @@ end local function load_vca(source, filepath) local raw_track = parse_track(xml.parse_vcd(source, "track")) - return animation.compile_track(raw_track, filepath) + return internals.compile_animation_track(raw_track, filepath) end function internals.load_vca_animation(filepath, source, identifier) diff --git a/res/scripts/stdlib.lua b/res/scripts/stdlib.lua index dee3bf579..3573bda07 100644 --- a/res/scripts/stdlib.lua +++ b/res/scripts/stdlib.lua @@ -334,6 +334,7 @@ world.raycast = entities.__world_raycast entities.__world_raycast = nil animation = require "core:animation" +require "core:internal/animation_codegen" require "core:internal/formats/vca" __vc_scripts_registry = require "core:internal/scripts_registry" From aa6ac5f5bd024fc21d7550f3c1caed10980de439 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 10 Sep 2026 00:14:15 +0300 Subject: [PATCH 13/56] add animation.stop --- res/modules/animation.lua | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/res/modules/animation.lua b/res/modules/animation.lua index c42b15d25..fb0a915f2 100644 --- a/res/modules/animation.lua +++ b/res/modules/animation.lua @@ -68,13 +68,26 @@ end local running_actions = {} local playing_tracks = {} +local next_track_id = 1 function this.action(func) table.insert(running_actions, coroutine.create(func)) end function this.play(name, target) - table.insert(playing_tracks, {name=name, target=target, timer=0.0}) + local id = next_track_id + table.insert(playing_tracks, {id=id, name=name, target=target, timer=0.0}) + next_track_id = id + 1 + return id +end + +function this.stop(id) + for i, entry in pairs(playing_tracks) do + if entry.id == id then + table.remove(playing_tracks, i) + return + end + end end function internals.on_animation_frame() From 21bd5ba1b2fbccd3b9ce6297d583530ffee53a1a Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 10 Sep 2026 19:09:49 +0300 Subject: [PATCH 14/56] update animation api --- res/modules/animation.lua | 48 ++++++++++++++-------- res/modules/internal/animation_codegen.lua | 2 +- res/modules/internal/formats/vca.lua | 2 +- 3 files changed, 33 insertions(+), 19 deletions(-) diff --git a/res/modules/animation.lua b/res/modules/animation.lua index fb0a915f2..3f6acf351 100644 --- a/res/modules/animation.lua +++ b/res/modules/animation.lua @@ -68,26 +68,40 @@ end local running_actions = {} local playing_tracks = {} -local next_track_id = 1 function this.action(func) table.insert(running_actions, coroutine.create(func)) end -function this.play(name, target) - local id = next_track_id - table.insert(playing_tracks, {id=id, name=name, target=target, timer=0.0}) - next_track_id = id + 1 - return id -end - -function this.stop(id) - for i, entry in pairs(playing_tracks) do - if entry.id == id then - table.remove(playing_tracks, i) - return +local PlayingTrack = { + __index = { + stop = function(self) + table.remove_value(playing_tracks, self) + self.__timer = 0.0 + self.__playing = false + end, + pause = function(self) + table.remove_value(playing_tracks, self) + self.__playing = false + end, + resume = function(self) + if not self.__playing then + table.insert(playing_tracks, self) + self.__playing = true + end end - end + } +} + +function this.play(name, target) + local track = setmetatable({ + name = name, + target = target, + __timer = 0.0, + __playing = true, + }, PlayingTrack) + table.insert(playing_tracks, track) + return track end function internals.on_animation_frame() @@ -109,11 +123,11 @@ function internals.on_animation_frame() debug.error("animation track not found: "..track_info.name) table.remove(playing_tracks, i) else - track_info.timer = track_info.timer + delta - if track_info.timer > track.duration then + track_info.__timer = track_info.__timer + delta + if track_info.__timer > track.duration then table.remove(playing_tracks, i) else - track.func(track_info.target, track_info.timer) + track.func(track_info.target, track_info.__timer) end end end diff --git a/res/modules/internal/animation_codegen.lua b/res/modules/internal/animation_codegen.lua index 3301f401f..0b2cd7ce3 100644 --- a/res/modules/internal/animation_codegen.lua +++ b/res/modules/internal/animation_codegen.lua @@ -182,7 +182,7 @@ local function codegen_track(raw_track, lineset, memoised, keysets, use_tsf) local axis_names = {"X", "Y", "Z"} local axis_indices = {X=1, Y=2, Z=3} - local rotation_order = raw_track.rotation_order + local rotation_order = raw_track.rotation_order or "XYZ" for i=1,3 do local axis = axis_indices[rotation_order[i]] local var = rotation[axis] diff --git a/res/modules/internal/formats/vca.lua b/res/modules/internal/formats/vca.lua index 051066ecd..fc910a8d7 100644 --- a/res/modules/internal/formats/vca.lua +++ b/res/modules/internal/formats/vca.lua @@ -24,7 +24,7 @@ local function parse_configure(raw_track, node) elseif node.duration then raw_track.duration = node.duration end - raw_track.rotation_order = string.upper(node["rotation-order"]) or "XYZ" + raw_track.rotation_order = string.upper(node["rotation-order"]) end local function parse_curve(line, node) From 456d1c7b382bb0063258aa2695c8d002fa6c5e23 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 10 Sep 2026 19:27:35 +0300 Subject: [PATCH 15/56] update doc/ru/vca.md --- doc/ru/vca.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/doc/ru/vca.md b/doc/ru/vca.md index eb1f77b07..795541dc1 100644 --- a/doc/ru/vca.md +++ b/doc/ru/vca.md @@ -38,6 +38,18 @@ VCA файл состоит из набора директив, порядок Мета-информация применяется глобально. Поведение не зависит от её положения в файле, но рекомендуется располагать её в начале. +## Дополнительно + +- `curve` - объявляет пользовательский тип кривой. Указывается имя кривой `name` и выражение вычисления значения `func`. + +В выражении доступны два ключа `kl`, `kr` и значение `t` в диапазоне [0..1]. + +Пример: + +`@curve test-linear func (kl.value + (kr.value - kl.value) * t)` + +Пользовательские кривые указываются с префиксом `.`. Пример: `@move by y curve .test-linear {...}`. + ## Кривые с ключевыми кадрами Требуется через атрибут `curve` указать тип кривой: From 9226f362a77f7ace5ca9633373f9f6ee904328af Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 10 Sep 2026 20:01:00 +0300 Subject: [PATCH 16/56] cleanup network.* functions --- res/scripts/classes.lua | 48 +++++------- src/logic/scripting/lua/libs/libnetwork.cpp | 81 +-------------------- 2 files changed, 21 insertions(+), 108 deletions(-) diff --git a/res/scripts/classes.lua b/res/scripts/classes.lua index c6e25b0b9..0f9f1ce0a 100644 --- a/res/scripts/classes.lua +++ b/res/scripts/classes.lua @@ -101,17 +101,11 @@ local _http_response_callbacks = {} local _http_error_callbacks = {} local http_request = network.__request -local http_get = network.__get -local http_get_binary = network.__get_binary -local http_post = network.__post local open_tcp = network.__open_tcp local open_udp = network.__open_udp local connect_tcp = network.__connect_tcp local connect_udp = network.__connect_udp network.__request = nil -network.__get = nil -network.__get_binary = nil -network.__post = nil network.__open_tcp = nil network.__open_udp = nil network.__connect_tcp = nil @@ -130,33 +124,31 @@ end network.request = request network.get = function(url, callback, errorCallback, headers) - local id = http_get(url, headers) - if callback then - _http_response_callbacks[id] = callback - end - if errorCallback then - _http_error_callbacks[id] = errorCallback - end + return request(url, { + headers = headers, + on_response = callback, + on_error = errorCallback, + }) end network.get_binary = function(url, callback, errorCallback, headers) - local id = http_get_binary(url, headers) - if callback then - _http_response_callbacks[id] = callback - end - if errorCallback then - _http_error_callbacks[id] = errorCallback - end + return request(url, { + headers = headers, + on_response = callback and (function (response) return callback(Bytearray(response)) end), + on_error = errorCallback + }) end -network.post = function(url, data, callback, errorCallback, headers) - local id = http_post(url, data, headers) - if callback then - _http_response_callbacks[id] = callback - end - if errorCallback then - _http_error_callbacks[id] = errorCallback - end +network.post = function(url, body, callback, errorCallback, headers) + return request(url, { + method = "POST", + headers = table.extend({ + "Content-Type: application/json" + }, headers), + body = body, + on_response = callback, + on_error = errorCallback, + }) end network.tcp_open = function (port, handler) diff --git a/src/logic/scripting/lua/libs/libnetwork.cpp b/src/logic/scripting/lua/libs/libnetwork.cpp index 318dfcc7c..b10d12b4b 100644 --- a/src/logic/scripting/lua/libs/libnetwork.cpp +++ b/src/logic/scripting/lua/libs/libnetwork.cpp @@ -94,7 +94,7 @@ static int l_request(lua::State* L, network::Network& network) { throw std::runtime_error("table expected as argument #2"); } if (lua::getfield(L, "method", 2)) { - request.method = util::upper_case(lua::require_string(L, -1)); + request.method = lua::require_string(L, -1); lua::pop(L); } if (lua::getfield(L, "headers", 2)) { @@ -115,82 +115,6 @@ static int l_request(lua::State* L, network::Network& network) { return lua::pushinteger(L, currentRequestId); } -static int perform_get(lua::State* L, network::Network& network, bool binary) { - std::string url(lua::require_lstring(L, 1)); - auto headers = read_headers(L, 2); - - int currentRequestId = request_id++; - - network::HttpRequest request {}; - request.url = std::move(url); - request.onResponse = - [currentRequestId, binary](std::vector bytes) { - push_event(NetworkEvent( - RESPONSE, - ResponseEventDto { - 200, binary, currentRequestId, std::move(bytes)} - )); - }; - request.onReject = - [currentRequestId, binary](int code, std::vector bytes) { - push_event(NetworkEvent( - RESPONSE, - ResponseEventDto { - code, binary, currentRequestId, std::move(bytes)} - )); - }; - request.headers = std::move(headers); - network.request(std::move(request)); - return lua::pushinteger(L, currentRequestId); -} - -static int l_get(lua::State* L, network::Network& network) { - return perform_get(L, network, false); -} - -static int l_get_binary(lua::State* L, network::Network& network) { - return perform_get(L, network, true); -} - -static int l_post(lua::State* L, network::Network& network) { - std::string url(lua::require_lstring(L, 1)); - auto data = lua::tovalue(L, 2); - - std::string string; - if (data.isString()) { - string = data.asString(); - } else { - string = json::stringify(data, false); - } - - auto headers = read_headers(L, 3); - int currentRequestId = request_id++; - - network::HttpRequest request {}; - request.url = std::move(url); - request.body = string; - request.headers = std::move(headers); - request.headers.emplace_back("Content-Type: application/json"); - request.onResponse = - [currentRequestId](std::vector bytes) { - push_event(NetworkEvent( - RESPONSE, - ResponseEventDto { - 200, false, currentRequestId, std::move(bytes)} - )); - }; - request.onReject = - [currentRequestId](int code, std::vector bytes) { - push_event(NetworkEvent( - RESPONSE, - ResponseEventDto { - code, false, currentRequestId, std::move(bytes)} - )); - }; - network.request(std::move(request)); - return lua::pushinteger(L, currentRequestId); -} - static int l_close(lua::State* L, network::Network& network) { u64id_t id = lua::tointeger(L, 1); if (auto connection = network.getConnection(id, false)) { @@ -598,9 +522,6 @@ int wrap(lua_State* L) { const luaL_Reg networklib[] = { {"__request", wrap}, - {"__get", wrap}, - {"__get_binary", wrap}, - {"__post", wrap}, {"get_total_upload", wrap}, {"get_total_download", wrap}, {"find_free_port", wrap}, From 946728e54e65cf1e14fe7f5d277f75c7008ec45e Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 10 Sep 2026 20:38:50 +0300 Subject: [PATCH 17/56] fix --- res/scripts/classes.lua | 6 +++++- src/logic/scripting/lua/libs/libnetwork.cpp | 13 +++++++++++++ src/network/Curl.cpp | 11 ++++++++++- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/res/scripts/classes.lua b/res/scripts/classes.lua index 0f9f1ce0a..b044aa478 100644 --- a/res/scripts/classes.lua +++ b/res/scripts/classes.lua @@ -125,17 +125,21 @@ network.request = request network.get = function(url, callback, errorCallback, headers) return request(url, { + method = "GET", headers = headers, on_response = callback, on_error = errorCallback, + follow_location = true, }) end network.get_binary = function(url, callback, errorCallback, headers) return request(url, { + method = "GET", headers = headers, on_response = callback and (function (response) return callback(Bytearray(response)) end), - on_error = errorCallback + on_error = errorCallback, + follow_location = true, }) end diff --git a/src/logic/scripting/lua/libs/libnetwork.cpp b/src/logic/scripting/lua/libs/libnetwork.cpp index b10d12b4b..d8c9e7c98 100644 --- a/src/logic/scripting/lua/libs/libnetwork.cpp +++ b/src/logic/scripting/lua/libs/libnetwork.cpp @@ -111,6 +111,19 @@ static int l_request(lua::State* L, network::Network& network) { } int currentRequestId = request_id++; + request.onResponse = [currentRequestId](std::vector bytes) { + push_event(NetworkEvent( + RESPONSE, + ResponseEventDto {200, false, currentRequestId, std::move(bytes)} + )); + }; + request.onReject = [currentRequestId](int code, std::vector bytes) { + push_event(NetworkEvent( + RESPONSE, + ResponseEventDto {code, false, currentRequestId, std::move(bytes)} + )); + }; + network.request(std::move(request)); return lua::pushinteger(L, currentRequestId); } diff --git a/src/network/Curl.cpp b/src/network/Curl.cpp index 872e6d680..3bbeb3489 100644 --- a/src/network/Curl.cpp +++ b/src/network/Curl.cpp @@ -80,6 +80,9 @@ public: curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buffer); curl_easy_setopt(curl, CURLOPT_USERAGENT, "curl/7.81.0"); +#ifndef NDEBUG + // curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); +#endif if (request.maxSize == 0) { curl_easy_setopt( curl, CURLOPT_MAXFILESIZE, std::numeric_limits::max() @@ -119,7 +122,8 @@ public: if(msg->msg == CURLMSG_DONE) { curl_multi_remove_handle(multiHandle, curl); } - int response; + int response = -1; + CURLcode result = msg->data.result; curl_easy_getinfo(msg->easy_handle, CURLINFO_RESPONSE_CODE, &response); if (response == HTTP_OK) { long size; @@ -133,6 +137,11 @@ public: if (onResponse) { onResponse(std::move(buffer)); } + } else if (response == 0) { + logger.error() << curl_easy_strerror(result) << " (" << url << ")"; + if (onReject) { + onReject(response, {}); + } } else { logger.error() << "response code " << response << " (" << url << ")" From 0608a9ad3d5b65c4419b1342fa9cce97d294c1cb Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 10 Sep 2026 20:42:22 +0300 Subject: [PATCH 18/56] update request error handling --- src/network/Curl.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/network/Curl.cpp b/src/network/Curl.cpp index 3bbeb3489..55e98988b 100644 --- a/src/network/Curl.cpp +++ b/src/network/Curl.cpp @@ -138,9 +138,15 @@ public: onResponse(std::move(buffer)); } } else if (response == 0) { - logger.error() << curl_easy_strerror(result) << " (" << url << ")"; + auto message = std::string(curl_easy_strerror(result)); + logger.error() << message << " (" << url << ")"; if (onReject) { - onReject(response, {}); + onReject( + response, + std::vector( + message.data(), message.data() + message.size() + ) + ); } } else { logger.error() From effa850337c4c1e243825255db605ac4b834b6ab Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 10 Sep 2026 21:08:12 +0300 Subject: [PATCH 19/56] add HttpRequest::verifySSL --- src/network/Curl.cpp | 4 +++- src/network/commons.hpp | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/network/Curl.cpp b/src/network/Curl.cpp index 55e98988b..ca64f43f3 100644 --- a/src/network/Curl.cpp +++ b/src/network/Curl.cpp @@ -71,10 +71,12 @@ public: hs = curl_slist_append(hs, header.c_str()); } + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, request.body.length()); if (!request.body.empty()) { - curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, request.body.length()); curl_easy_setopt(curl, CURLOPT_COPYPOSTFIELDS, request.body.data()); } + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, request.verifySSL); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, request.verifySSL); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, hs); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, request.followLocation); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); diff --git a/src/network/commons.hpp b/src/network/commons.hpp index 614821404..46745bfbb 100644 --- a/src/network/commons.hpp +++ b/src/network/commons.hpp @@ -26,6 +26,7 @@ namespace network { OnResponse onResponse; OnReject onReject; bool followLocation = false; + bool verifySSL = true; long maxSize = -1; }; From 9510e06a26bf0375d9301ac4203d57647f88a119 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 10 Sep 2026 22:29:34 +0300 Subject: [PATCH 20/56] update Curl.cpp --- src/network/Curl.cpp | 105 +++++++++++++++++++++---------------------- 1 file changed, 52 insertions(+), 53 deletions(-) diff --git a/src/network/Curl.cpp b/src/network/Curl.cpp index ca64f43f3..52c2e4132 100644 --- a/src/network/Curl.cpp +++ b/src/network/Curl.cpp @@ -23,27 +23,27 @@ static size_t write_callback( return size * nmemb; } +struct ProcessingRequest { + CURL* curl; + HttpRequest request; + std::vector buffer; +}; + class CurlRequests : public Requests { CURLM* multiHandle; - CURL* curl; + std::vector> requests; size_t totalUpload = 0; size_t totalDownload = 0; - - OnResponse onResponse; - OnReject onReject; - std::vector buffer; - std::string url; - - std::queue requests; public: - CurlRequests(CURLM* multiHandle, CURL* curl) - : multiHandle(multiHandle), curl(curl) { + CurlRequests(CURLM* multiHandle) : multiHandle(multiHandle) { } virtual ~CurlRequests() { - curl_multi_remove_handle(multiHandle, curl); - curl_easy_cleanup(curl); + for (auto& entry : requests) { + curl_multi_remove_handle(multiHandle, entry->curl); + curl_easy_cleanup(entry->curl); + } curl_multi_cleanup(multiHandle); } @@ -52,17 +52,12 @@ public: } void processRequest(HttpRequest request) { - if (!url.empty()) { - requests.push(request); - return; - } - onResponse = request.onResponse; - onReject = request.onReject; - url = request.url; + auto curl = curl_easy_init(); - buffer.clear(); + auto entry = std::make_unique(); + entry->curl = curl; - curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_URL, request.url.c_str()); curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, request.method.c_str()); curl_slist* hs = nullptr; @@ -80,7 +75,7 @@ public: curl_easy_setopt(curl, CURLOPT_HTTPHEADER, hs); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, request.followLocation); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buffer); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &entry->buffer); curl_easy_setopt(curl, CURLOPT_USERAGENT, "curl/7.81.0"); #ifndef NDEBUG // curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); @@ -97,12 +92,13 @@ public: CURLMcode res = curl_multi_perform(multiHandle, &running); if (res != CURLM_OK) { auto message = curl_multi_strerror(res); - logger.error() << message << " (" << url << ")"; - if (onReject) { - onReject(HTTP_BAD_GATEWAY, {}); + logger.error() << message << " (" << request.url << ")"; + if (request.onReject) { + request.onReject(HTTP_BAD_GATEWAY, {}); } - url = ""; } + entry->request = std::move(request); + requests.push_back(std::move(entry)); } void update() override { @@ -112,15 +108,24 @@ public: CURLMcode res = curl_multi_perform(multiHandle, &running); if (res != CURLM_OK) { auto message = curl_multi_strerror(res); - logger.error() << message << " (" << url << ")"; - if (onReject) { - onReject(HTTP_BAD_GATEWAY, {}); - } - curl_multi_remove_handle(multiHandle, curl); - url = ""; + logger.error() << message; return; } if ((msg = curl_multi_info_read(multiHandle, &messagesLeft)) != nullptr) { + auto curl = msg->easy_handle; + auto found = std::find_if( + requests.begin(), requests.end(), + [curl](const std::unique_ptr& entry) { + return entry->curl == curl; + } + ); + if (found == requests.end()) { + logger.error() << "could not find request for cURL handle"; + return; + } + auto& entry = *found; + auto& req = entry->request; + if(msg->msg == CURLMSG_DONE) { curl_multi_remove_handle(multiHandle, curl); } @@ -135,15 +140,15 @@ public: if (!curl_easy_getinfo(curl, CURLINFO_HEADER_SIZE, &size)) { totalDownload += size; } - totalDownload += buffer.size(); - if (onResponse) { - onResponse(std::move(buffer)); + totalDownload += entry->buffer.size(); + if (req.onResponse) { + req.onResponse(std::move(entry->buffer)); } } else if (response == 0) { auto message = std::string(curl_easy_strerror(result)); - logger.error() << message << " (" << url << ")"; - if (onReject) { - onReject( + logger.error() << message << " (" << req.url << ")"; + if (req.onReject) { + req.onReject( response, std::vector( message.data(), message.data() + message.size() @@ -152,21 +157,15 @@ public: } } else { logger.error() - << "response code " << response << " (" << url << ")" - << (buffer.empty() + << "response code " << response << " (" << req.url << ")" + << (entry->buffer.empty() ? "" - : std::to_string(buffer.size()) + " byte(s)"); - totalDownload += buffer.size(); - if (onReject) { - onReject(response, std::move(buffer)); + : std::to_string(entry->buffer.size()) + " byte(s)"); + totalDownload += entry->buffer.size(); + if (req.onReject) { + req.onReject(response, std::move(entry->buffer)); } } - url = ""; - } - if (url.empty() && !requests.empty()) { - auto request = std::move(requests.front()); - requests.pop(); - processRequest(std::move(request)); } } @@ -181,14 +180,14 @@ public: static std::unique_ptr create() { auto curl = curl_easy_init(); if (curl == nullptr) { - throw std::runtime_error("could not initialzie cURL"); + throw std::runtime_error("could not initialize cURL"); } auto multiHandle = curl_multi_init(); if (multiHandle == nullptr) { curl_easy_cleanup(curl); - throw std::runtime_error("could not initialzie cURL-multi"); + throw std::runtime_error("could not initialize cURL-multi"); } - return std::make_unique(multiHandle, curl); + return std::make_unique(multiHandle); } }; From f44d3355f90b94703a8c0ae6d4c05d756ab1fedb Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 10 Sep 2026 22:45:18 +0300 Subject: [PATCH 21/56] fix curl handles lifetime --- src/network/Curl.cpp | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/network/Curl.cpp b/src/network/Curl.cpp index 52c2e4132..42d4817c8 100644 --- a/src/network/Curl.cpp +++ b/src/network/Curl.cpp @@ -24,9 +24,19 @@ static size_t write_callback( } struct ProcessingRequest { + CURLM* multiHandle; CURL* curl; HttpRequest request; std::vector buffer; + + ProcessingRequest(CURLM* multiHandle) : multiHandle(multiHandle) { + curl = curl_easy_init(); + } + + ~ProcessingRequest() { + curl_multi_remove_handle(multiHandle, curl); + curl_easy_cleanup(curl); + } }; class CurlRequests : public Requests { @@ -40,10 +50,7 @@ public: } virtual ~CurlRequests() { - for (auto& entry : requests) { - curl_multi_remove_handle(multiHandle, entry->curl); - curl_easy_cleanup(entry->curl); - } + requests.clear(); curl_multi_cleanup(multiHandle); } @@ -52,10 +59,8 @@ public: } void processRequest(HttpRequest request) { - auto curl = curl_easy_init(); - - auto entry = std::make_unique(); - entry->curl = curl; + auto entry = std::make_unique(multiHandle); + auto curl = entry->curl; curl_easy_setopt(curl, CURLOPT_URL, request.url.c_str()); curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, request.method.c_str()); @@ -123,9 +128,11 @@ public: logger.error() << "could not find request for cURL handle"; return; } - auto& entry = *found; + auto entry = std::move(*found); auto& req = entry->request; + requests.erase(found); + if(msg->msg == CURLMSG_DONE) { curl_multi_remove_handle(multiHandle, curl); } From a5f9eafe23435988ecdbf20023a17e3cdb055401 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 11 Sep 2026 21:17:01 +0300 Subject: [PATCH 22/56] add HttpRequest::timeoutMs --- src/network/Curl.cpp | 3 +++ src/network/commons.hpp | 1 + 2 files changed, 4 insertions(+) diff --git a/src/network/Curl.cpp b/src/network/Curl.cpp index 42d4817c8..fd1b0ad14 100644 --- a/src/network/Curl.cpp +++ b/src/network/Curl.cpp @@ -82,6 +82,9 @@ public: curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &entry->buffer); curl_easy_setopt(curl, CURLOPT_USERAGENT, "curl/7.81.0"); + if (request.timeoutMs > 0) { + curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, request.timeoutMs); + } #ifndef NDEBUG // curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); #endif diff --git a/src/network/commons.hpp b/src/network/commons.hpp index 46745bfbb..79e4bdb9d 100644 --- a/src/network/commons.hpp +++ b/src/network/commons.hpp @@ -28,6 +28,7 @@ namespace network { bool followLocation = false; bool verifySSL = true; long maxSize = -1; + long timeoutMs = 0; }; class Requests { From fc877f09cbe4338fa71086dd263cc8ad475487b0 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 11 Sep 2026 21:19:13 +0300 Subject: [PATCH 23/56] add 'timeout_ms' to network.request --- src/logic/scripting/lua/libs/libnetwork.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/logic/scripting/lua/libs/libnetwork.cpp b/src/logic/scripting/lua/libs/libnetwork.cpp index d8c9e7c98..577eee7a9 100644 --- a/src/logic/scripting/lua/libs/libnetwork.cpp +++ b/src/logic/scripting/lua/libs/libnetwork.cpp @@ -109,6 +109,10 @@ static int l_request(lua::State* L, network::Network& network) { request.followLocation = lua::toboolean(L, -1); lua::pop(L); } + if (lua::getfield(L, "timeout_ms", 2)) { + request.timeoutMs = lua::tointeger(L, -1); + lua::pop(L); + } int currentRequestId = request_id++; request.onResponse = [currentRequestId](std::vector bytes) { From 58c974a3a1fe6acfbc44ae824aa258517f839656 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 13 Sep 2026 16:16:59 +0300 Subject: [PATCH 24/56] add HttpResponse --- src/logic/scripting/lua/libs/libnetwork.cpp | 16 ++- src/network/Curl.cpp | 136 ++++++++++++-------- src/network/commons.hpp | 12 +- test/network/curltest.cpp | 7 +- 4 files changed, 108 insertions(+), 63 deletions(-) diff --git a/src/logic/scripting/lua/libs/libnetwork.cpp b/src/logic/scripting/lua/libs/libnetwork.cpp index 577eee7a9..37ef3029d 100644 --- a/src/logic/scripting/lua/libs/libnetwork.cpp +++ b/src/logic/scripting/lua/libs/libnetwork.cpp @@ -115,16 +115,24 @@ static int l_request(lua::State* L, network::Network& network) { } int currentRequestId = request_id++; - request.onResponse = [currentRequestId](std::vector bytes) { + request.onResponse = [currentRequestId](network::HttpResponse response) { push_event(NetworkEvent( RESPONSE, - ResponseEventDto {200, false, currentRequestId, std::move(bytes)} + ResponseEventDto { + response.status, + false, + currentRequestId, + std::move(response.body)} )); }; - request.onReject = [currentRequestId](int code, std::vector bytes) { + request.onReject = [currentRequestId](network::HttpResponse response) { push_event(NetworkEvent( RESPONSE, - ResponseEventDto {code, false, currentRequestId, std::move(bytes)} + ResponseEventDto { + response.status, + false, + currentRequestId, + std::move(response.body)} )); }; diff --git a/src/network/Curl.cpp b/src/network/Curl.cpp index fd1b0ad14..afa4be009 100644 --- a/src/network/Curl.cpp +++ b/src/network/Curl.cpp @@ -23,11 +23,28 @@ static size_t write_callback( return size * nmemb; } +static size_t header_callback( + char* buffer, size_t size, size_t nitems, void* userdata +) { + auto* headers = static_cast*>(userdata); + size_t len = size * nitems; + std::string header(buffer, len); + + while (!header.empty() && + (header.back() == '\r' || header.back() == '\n')) { + header.pop_back(); + } + + headers->push_back(std::move(header)); + return len; +} + struct ProcessingRequest { CURLM* multiHandle; CURL* curl; HttpRequest request; std::vector buffer; + std::vector headers; ProcessingRequest(CURLM* multiHandle) : multiHandle(multiHandle) { curl = curl_easy_init(); @@ -81,6 +98,8 @@ public: curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, request.followLocation); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &entry->buffer); + curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header_callback); + curl_easy_setopt(curl, CURLOPT_HEADERDATA, &entry->headers); curl_easy_setopt(curl, CURLOPT_USERAGENT, "curl/7.81.0"); if (request.timeoutMs > 0) { curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, request.timeoutMs); @@ -102,7 +121,7 @@ public: auto message = curl_multi_strerror(res); logger.error() << message << " (" << request.url << ")"; if (request.onReject) { - request.onReject(HTTP_BAD_GATEWAY, {}); + request.onReject({HTTP_BAD_GATEWAY, {}, {}}); } } entry->request = std::move(request); @@ -112,69 +131,78 @@ public: void update() override { int messagesLeft; int running; - CURLMsg* msg; CURLMcode res = curl_multi_perform(multiHandle, &running); if (res != CURLM_OK) { auto message = curl_multi_strerror(res); logger.error() << message; return; } - if ((msg = curl_multi_info_read(multiHandle, &messagesLeft)) != nullptr) { - auto curl = msg->easy_handle; - auto found = std::find_if( - requests.begin(), requests.end(), - [curl](const std::unique_ptr& entry) { - return entry->curl == curl; - } - ); - if (found == requests.end()) { - logger.error() << "could not find request for cURL handle"; - return; + CURLMsg* msg = curl_multi_info_read(multiHandle, &messagesLeft); + if (msg == nullptr) { + return; + } + auto curl = msg->easy_handle; + auto found = std::find_if( + requests.begin(), + requests.end(), + [curl](const std::unique_ptr& entry) { + return entry->curl == curl; } - auto entry = std::move(*found); - auto& req = entry->request; + ); + if (found == requests.end()) { + logger.error() << "could not find request for cURL handle"; + return; + } + auto entry = std::move(*found); + auto& req = entry->request; - requests.erase(found); + requests.erase(found); - if(msg->msg == CURLMSG_DONE) { - curl_multi_remove_handle(multiHandle, curl); + if (msg->msg == CURLMSG_DONE) { + curl_multi_remove_handle(multiHandle, curl); + } + int response = -1; + CURLcode result = msg->data.result; + curl_easy_getinfo(msg->easy_handle, CURLINFO_RESPONSE_CODE, &response); + auto headers = std::move(entry->headers); + if (response == HTTP_OK) { + long size; + if (!curl_easy_getinfo(curl, CURLINFO_REQUEST_SIZE, &size)) { + totalUpload += size; } - int response = -1; - CURLcode result = msg->data.result; - curl_easy_getinfo(msg->easy_handle, CURLINFO_RESPONSE_CODE, &response); - if (response == HTTP_OK) { - long size; - if (!curl_easy_getinfo(curl, CURLINFO_REQUEST_SIZE, &size)) { - totalUpload += size; - } - if (!curl_easy_getinfo(curl, CURLINFO_HEADER_SIZE, &size)) { - totalDownload += size; - } - totalDownload += entry->buffer.size(); - if (req.onResponse) { - req.onResponse(std::move(entry->buffer)); - } - } else if (response == 0) { - auto message = std::string(curl_easy_strerror(result)); - logger.error() << message << " (" << req.url << ")"; - if (req.onReject) { - req.onReject( - response, - std::vector( - message.data(), message.data() + message.size() - ) - ); - } - } else { - logger.error() - << "response code " << response << " (" << req.url << ")" - << (entry->buffer.empty() - ? "" - : std::to_string(entry->buffer.size()) + " byte(s)"); - totalDownload += entry->buffer.size(); - if (req.onReject) { - req.onReject(response, std::move(entry->buffer)); - } + if (!curl_easy_getinfo(curl, CURLINFO_HEADER_SIZE, &size)) { + totalDownload += size; + } + totalDownload += entry->buffer.size(); + if (req.onResponse) { + req.onResponse({ + response, + std::move(headers), + std::move(entry->buffer), + }); + } + } else if (response == 0) { + auto message = std::string(curl_easy_strerror(result)); + logger.error() << message << " (" << req.url << ")"; + if (req.onReject) { + req.onReject( + {response, + std::move(headers), + std::vector( + message.data(), message.data() + message.size() + )} + ); + } + } else { + logger.error() << "response code " << response << " (" << req.url + << ")" + << (entry->buffer.empty() + ? "" + : std::to_string(entry->buffer.size()) + + " byte(s)"); + totalDownload += entry->buffer.size(); + if (req.onReject) { + req.onReject({response, {}, std::move(entry->buffer)}); } } } diff --git a/src/network/commons.hpp b/src/network/commons.hpp index 79e4bdb9d..15fe69850 100644 --- a/src/network/commons.hpp +++ b/src/network/commons.hpp @@ -10,8 +10,10 @@ #include namespace network { - using OnResponse = std::function)>; - using OnReject = std::function)>; + struct HttpResponse; + + using OnResponse = std::function; + using OnReject = std::function; using ConnectCallback = std::function; using ConnectErrorCallback = std::function; using ServerDatagramCallback = std::function; @@ -31,6 +33,12 @@ namespace network { long timeoutMs = 0; }; + struct HttpResponse { + int status; + std::vector headers; + std::vector body; + }; + class Requests { public: virtual ~Requests() {} diff --git a/test/network/curltest.cpp b/test/network/curltest.cpp index 44f87d17e..e7fc4840d 100644 --- a/test/network/curltest.cpp +++ b/test/network/curltest.cpp @@ -10,11 +10,12 @@ TEST(curltest, curltest) { network::HttpRequest request {}; request.url = "https://raw.githubusercontent.com/MihailRis/VoxelEngine-Cpp/refs/" "heads/curl/res/content/base/blocks/lamp.json"; - request.onResponse = [](std::vector data) { - if (data.empty()) { + request.onResponse = [](network::HttpResponse response) { + if (response.body.empty()) { return; } - auto view = std::string_view(data.data(), data.size()); + auto view = + std::string_view(response.body.data(), response.body.size()); auto value = json::parse(view); std::cout << value << std::endl; }; From 432687c6abd24d77757b616ed6cfbd666fec31f8 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 13 Sep 2026 17:55:12 +0300 Subject: [PATCH 25/56] cleanup request --- src/logic/scripting/lua/libs/libnetwork.cpp | 10 ---- src/network/Curl.cpp | 59 ++++++++------------- src/network/commons.hpp | 2 - 3 files changed, 22 insertions(+), 49 deletions(-) diff --git a/src/logic/scripting/lua/libs/libnetwork.cpp b/src/logic/scripting/lua/libs/libnetwork.cpp index 37ef3029d..475158dea 100644 --- a/src/logic/scripting/lua/libs/libnetwork.cpp +++ b/src/logic/scripting/lua/libs/libnetwork.cpp @@ -125,16 +125,6 @@ static int l_request(lua::State* L, network::Network& network) { std::move(response.body)} )); }; - request.onReject = [currentRequestId](network::HttpResponse response) { - push_event(NetworkEvent( - RESPONSE, - ResponseEventDto { - response.status, - false, - currentRequestId, - std::move(response.body)} - )); - }; network.request(std::move(request)); return lua::pushinteger(L, currentRequestId); diff --git a/src/network/Curl.cpp b/src/network/Curl.cpp index afa4be009..2acda8c8c 100644 --- a/src/network/Curl.cpp +++ b/src/network/Curl.cpp @@ -120,9 +120,10 @@ public: if (res != CURLM_OK) { auto message = curl_multi_strerror(res); logger.error() << message << " (" << request.url << ")"; - if (request.onReject) { - request.onReject({HTTP_BAD_GATEWAY, {}, {}}); + if (request.onResponse) { + request.onResponse({0, {}, {}}); } + return; } entry->request = std::move(request); requests.push_back(std::move(entry)); @@ -146,46 +147,25 @@ public: requests.begin(), requests.end(), [curl](const std::unique_ptr& entry) { - return entry->curl == curl; + return entry && entry->curl == curl; } ); - if (found == requests.end()) { + if (requests.empty() || found == requests.end()) { logger.error() << "could not find request for cURL handle"; return; } auto entry = std::move(*found); - auto& req = entry->request; - requests.erase(found); - if (msg->msg == CURLMSG_DONE) { - curl_multi_remove_handle(multiHandle, curl); - } int response = -1; CURLcode result = msg->data.result; curl_easy_getinfo(msg->easy_handle, CURLINFO_RESPONSE_CODE, &response); auto headers = std::move(entry->headers); - if (response == HTTP_OK) { - long size; - if (!curl_easy_getinfo(curl, CURLINFO_REQUEST_SIZE, &size)) { - totalUpload += size; - } - if (!curl_easy_getinfo(curl, CURLINFO_HEADER_SIZE, &size)) { - totalDownload += size; - } - totalDownload += entry->buffer.size(); - if (req.onResponse) { - req.onResponse({ - response, - std::move(headers), - std::move(entry->buffer), - }); - } - } else if (response == 0) { + if (response == 0) { auto message = std::string(curl_easy_strerror(result)); - logger.error() << message << " (" << req.url << ")"; - if (req.onReject) { - req.onReject( + logger.error() << message << " (" << entry->request.url << ")"; + if (entry->request.onResponse) { + entry->request.onResponse( {response, std::move(headers), std::vector( @@ -194,15 +174,20 @@ public: ); } } else { - logger.error() << "response code " << response << " (" << req.url - << ")" - << (entry->buffer.empty() - ? "" - : std::to_string(entry->buffer.size()) + - " byte(s)"); + long size; + if (!curl_easy_getinfo(curl, CURLINFO_REQUEST_SIZE, &size)) { + totalUpload += size; + } + if (!curl_easy_getinfo(curl, CURLINFO_HEADER_SIZE, &size)) { + totalDownload += size; + } totalDownload += entry->buffer.size(); - if (req.onReject) { - req.onReject({response, {}, std::move(entry->buffer)}); + if (entry->request.onResponse) { + entry->request.onResponse({ + response, + std::move(headers), + std::move(entry->buffer), + }); } } } diff --git a/src/network/commons.hpp b/src/network/commons.hpp index 15fe69850..cd98fbe00 100644 --- a/src/network/commons.hpp +++ b/src/network/commons.hpp @@ -13,7 +13,6 @@ namespace network { struct HttpResponse; using OnResponse = std::function; - using OnReject = std::function; using ConnectCallback = std::function; using ConnectErrorCallback = std::function; using ServerDatagramCallback = std::function; @@ -26,7 +25,6 @@ namespace network { std::vector headers; OnResponse onResponse; - OnReject onReject; bool followLocation = false; bool verifySSL = true; long maxSize = -1; From bb11a198f565fe19d9d28890364de7a8ef6b20c1 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 13 Sep 2026 19:41:26 +0300 Subject: [PATCH 26/56] fix CurlRequests::update --- dev/tests/network_http.lua | 2 +- src/network/Curl.cpp | 56 +++++++++++++++++++++++++++----------- 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/dev/tests/network_http.lua b/dev/tests/network_http.lua index 5861460de..12149de09 100644 --- a/dev/tests/network_http.lua +++ b/dev/tests/network_http.lua @@ -8,4 +8,4 @@ end, function (code) response_received = true end) -app.sleep_until(function () return response_received end, nil, 10) +app.sleep_until(function () return response_received end, nil, 5) diff --git a/src/network/Curl.cpp b/src/network/Curl.cpp index 2acda8c8c..6a7cebd1a 100644 --- a/src/network/Curl.cpp +++ b/src/network/Curl.cpp @@ -114,26 +114,49 @@ public: } else { curl_easy_setopt(curl, CURLOPT_MAXFILESIZE, request.maxSize); } - curl_multi_add_handle(multiHandle, curl); - int running; - CURLMcode res = curl_multi_perform(multiHandle, &running); - if (res != CURLM_OK) { - auto message = curl_multi_strerror(res); - logger.error() << message << " (" << request.url << ")"; - if (request.onResponse) { - request.onResponse({0, {}, {}}); + { + CURLMcode res = curl_multi_add_handle(multiHandle, curl); + if (res != CURLM_OK) { + auto message = curl_multi_strerror(res); + logger.error() << message << " (" << request.url << ")"; + if (request.onResponse) { + request.onResponse({0, {}, {}}); + } + return; + } + } + { + int running; + CURLMcode res = curl_multi_perform(multiHandle, &running); + if (res != CURLM_OK) { + auto message = curl_multi_strerror(res); + logger.error() << message << " (" << request.url << ")"; + if (request.onResponse) { + request.onResponse({0, {}, {}}); + } + return; } - return; } entry->request = std::move(request); requests.push_back(std::move(entry)); } void update() override { + if (CURLMcode res = curl_multi_poll( + multiHandle, + nullptr, + 0, + 1000, + nullptr + )) { + auto message = curl_multi_strerror(res); + logger.error() << message; + return; + } + int messagesLeft; int running; - CURLMcode res = curl_multi_perform(multiHandle, &running); - if (res != CURLM_OK) { + if (CURLMcode res = curl_multi_perform(multiHandle, &running)) { auto message = curl_multi_strerror(res); logger.error() << message; return; @@ -155,6 +178,7 @@ public: return; } auto entry = std::move(*found); + auto& req = entry->request; requests.erase(found); int response = -1; @@ -163,9 +187,9 @@ public: auto headers = std::move(entry->headers); if (response == 0) { auto message = std::string(curl_easy_strerror(result)); - logger.error() << message << " (" << entry->request.url << ")"; - if (entry->request.onResponse) { - entry->request.onResponse( + logger.error() << message << " (" << req.url << ")"; + if (req.onResponse) { + req.onResponse( {response, std::move(headers), std::vector( @@ -182,8 +206,8 @@ public: totalDownload += size; } totalDownload += entry->buffer.size(); - if (entry->request.onResponse) { - entry->request.onResponse({ + if (req.onResponse) { + req.onResponse({ response, std::move(headers), std::move(entry->buffer), From 5a9146f127bffea196b34d35488a04842dbf614b Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 13 Sep 2026 20:02:08 +0300 Subject: [PATCH 27/56] update workflows --- .github/workflows/appimage.yml | 2 +- .github/workflows/macos.yml | 2 +- .github/workflows/windows.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/appimage.yml b/.github/workflows/appimage.yml index f43254bd3..8d05f59b3 100644 --- a/.github/workflows/appimage.yml +++ b/.github/workflows/appimage.yml @@ -48,7 +48,7 @@ jobs: - name: Run tests run: ctest --test-dir build - name: Run engine tests - timeout-minutes: 1 + timeout-minutes: 5 run: | chmod +x build/VoxelEngine chmod +x AppDir/usr/bin/vctest diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index c3f5156f9..2f131de72 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -45,7 +45,7 @@ jobs: run: ctest --output-on-failure --test-dir build - name: Run engine tests - timeout-minutes: 1 + timeout-minutes: 5 run: | chmod +x build/VoxelEngine chmod +x AppDir/usr/bin/vctest diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index f248b6ab6..ffcb0c318 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -38,7 +38,7 @@ jobs: - name: Run engine tests run: | build/vctest/RelWithDebInfo/vctest.exe -e build/RelWithDebInfo/VoxelEngine.exe -d dev/tests -u build - timeout-minutes: 1 + timeout-minutes: 5 - name: Package for Windows run: | mkdir packaged From 4cb97d48a788be5e661ad3b69dd5f3280e32c1e7 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 13 Sep 2026 20:09:25 +0300 Subject: [PATCH 28/56] fix --- src/network/Curl.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/network/Curl.cpp b/src/network/Curl.cpp index 6a7cebd1a..65dce2f00 100644 --- a/src/network/Curl.cpp +++ b/src/network/Curl.cpp @@ -146,7 +146,7 @@ public: multiHandle, nullptr, 0, - 1000, + 0, nullptr )) { auto message = curl_multi_strerror(res); From 28b849229c7bd7d52c25aa1f009ff1d342f3f423 Mon Sep 17 00:00:00 2001 From: boolean-false Date: Mon, 14 Sep 2026 01:47:10 +0700 Subject: [PATCH 29/56] fix: validate content pack dependency versions --- res/layouts/pages/content.xml.lua | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/res/layouts/pages/content.xml.lua b/res/layouts/pages/content.xml.lua index 302f60644..268875874 100644 --- a/res/layouts/pages/content.xml.lua +++ b/res/layouts/pages/content.xml.lua @@ -208,7 +208,7 @@ function Version.__less(ver1, ver2) end function Version.__greater_or_equal(ver1, ver2) - return not Version.__less(ver2, ver1) + return not Version.__less(ver1, ver2) end function Version.__less_or_equal(ver1, ver2) @@ -227,6 +227,11 @@ function Version.compare(op, ver1, ver2) ver1 = string.split(ver1, ".") ver2 = string.split(ver2, ".") + for i = 1, 3 do + ver1[i] = tonumber(ver1[i]) or 0 + ver2[i] = tonumber(ver2[i]) or 0 + end + local comparison_func = Version.operators[op]; if comparison_func then @@ -252,7 +257,7 @@ end local function compare_version(op, dependent_version, actual_version) if Version.matches_pattern(dependent_version) and Version.matches_pattern(actual_version) then - return Version.compare(op, dep_ver, actual_version) + return Version.compare(op, actual_version, dependent_version) elseif dependent_version == "*" or dependent_version == actual_version then return true else From bd8d55f8bf0d56ff31c179cbda46776f3e5de0aa Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 14 Sep 2026 03:01:32 +0300 Subject: [PATCH 30/56] fix --- .github/workflows/macos.yml | 6 +++--- src/network/Curl.cpp | 8 ++++---- vctest/main.cpp | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 2f131de72..2bcb56dda 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -23,13 +23,13 @@ jobs: - name: Install dependencies from brew run: | - brew install glfw3 glew libpng openal-soft luajit libvorbis skypjack/entt/entt googletest glm freetype + brew install glfw3 glew libpng openal-soft luajit libvorbis skypjack/entt/entt googletest glm freetype - name: Configure run: | cmake -DOPENAL_INCLUDE_DIR=$(brew --prefix openal-soft)/include \ -DOPENAL_LIBRARY=$(brew --prefix openal-soft)/lib/libopenal.dylib \ - -S . -B build -DCMAKE_BUILD_TYPE=Release -DVOXELENGINE_BUILD_TESTS=ON -DVOXELENGINE_BUILD_APPDIR=1 -DVC_BUILD_NAME="${{ inputs.build_name }}" + -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo -DVOXELENGINE_BUILD_TESTS=ON -DVOXELENGINE_BUILD_APPDIR=1 -DVC_BUILD_NAME="${{ inputs.build_name }}" - name: Build run: | @@ -49,7 +49,7 @@ jobs: run: | chmod +x build/VoxelEngine chmod +x AppDir/usr/bin/vctest - AppDir/usr/bin/vctest -e build/VoxelEngine -d dev/tests -u build --output-always + AppDir/usr/bin/vctest -e build/VoxelEngine -d dev/tests -u build --output-always --memchecker "lldb -batch -o 'run' -o 'thread backtrace all' -o 'quit' -- " - name: Create DMG run: | mkdir VoxelEngineDmgContent diff --git a/src/network/Curl.cpp b/src/network/Curl.cpp index 65dce2f00..66ffe170a 100644 --- a/src/network/Curl.cpp +++ b/src/network/Curl.cpp @@ -126,7 +126,7 @@ public: } } { - int running; + int running; CURLMcode res = curl_multi_perform(multiHandle, &running); if (res != CURLM_OK) { auto message = curl_multi_strerror(res); @@ -181,7 +181,7 @@ public: auto& req = entry->request; requests.erase(found); - int response = -1; + long response = -1; CURLcode result = msg->data.result; curl_easy_getinfo(msg->easy_handle, CURLINFO_RESPONSE_CODE, &response); auto headers = std::move(entry->headers); @@ -190,7 +190,7 @@ public: logger.error() << message << " (" << req.url << ")"; if (req.onResponse) { req.onResponse( - {response, + {static_cast(response), std::move(headers), std::vector( message.data(), message.data() + message.size() @@ -208,7 +208,7 @@ public: totalDownload += entry->buffer.size(); if (req.onResponse) { req.onResponse({ - response, + static_cast(response), std::move(headers), std::move(entry->buffer), }); diff --git a/vctest/main.cpp b/vctest/main.cpp index ee2f15e5b..953d59aa9 100644 --- a/vctest/main.cpp +++ b/vctest/main.cpp @@ -183,8 +183,8 @@ static bool run_test(const Config& config, const fs::path& path, bool memcheck = auto name = path.stem(); std::stringstream ss; if (memcheck) { - ss << config.memchecker << " --log-file=" - << fix_path(memcheckLogFile.string()) << " "; + ss << config.memchecker << /*" --log-file=" + << fix_path(memcheckLogFile.string()) <<*/ " "; } ss << fs::canonical(config.executable) << " --headless"; ss << " --test " << fix_path(path.string()); From 5f37118f747e4061645675cf4aacbd9b97ccf3dd Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 14 Sep 2026 03:05:40 +0300 Subject: [PATCH 31/56] fix other issues --- src/network/Curl.cpp | 52 +++++++++++++++++++++++++++++++++----------- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/src/network/Curl.cpp b/src/network/Curl.cpp index 66ffe170a..1c6507d36 100644 --- a/src/network/Curl.cpp +++ b/src/network/Curl.cpp @@ -4,7 +4,6 @@ #define NOMINMAX #include -#include using namespace network; @@ -50,9 +49,41 @@ struct ProcessingRequest { curl = curl_easy_init(); } + ProcessingRequest(const ProcessingRequest&) = delete; + ProcessingRequest& operator=(const ProcessingRequest&) = delete; + + ProcessingRequest(ProcessingRequest&& other) noexcept + : multiHandle(other.multiHandle), + curl(other.curl), + request(std::move(other.request)), + buffer(std::move(other.buffer)), + headers(std::move(other.headers)) { + other.curl = nullptr; + other.multiHandle = nullptr; + } + + ProcessingRequest& operator=(ProcessingRequest&& other) noexcept { + if (this != &other) { + if (curl) { + curl_multi_remove_handle(multiHandle, curl); + curl_easy_cleanup(curl); + } + multiHandle = other.multiHandle; + curl = other.curl; + request = std::move(other.request); + buffer = std::move(other.buffer); + headers = std::move(other.headers); + other.curl = nullptr; + other.multiHandle = nullptr; + } + return *this; + } + ~ProcessingRequest() { - curl_multi_remove_handle(multiHandle, curl); - curl_easy_cleanup(curl); + if (curl) { + curl_multi_remove_handle(multiHandle, curl); + curl_easy_cleanup(curl); + } } }; @@ -88,14 +119,14 @@ public: hs = curl_slist_append(hs, header.c_str()); } - curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, request.body.length()); + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, static_cast(request.body.length())); if (!request.body.empty()) { curl_easy_setopt(curl, CURLOPT_COPYPOSTFIELDS, request.body.data()); } - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, request.verifySSL); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, request.verifySSL); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, static_cast(request.verifySSL)); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, static_cast(request.verifySSL)); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, hs); - curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, request.followLocation); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, static_cast(request.followLocation)); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &entry->buffer); curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header_callback); @@ -162,7 +193,7 @@ public: return; } CURLMsg* msg = curl_multi_info_read(multiHandle, &messagesLeft); - if (msg == nullptr) { + if (msg == nullptr || msg->msg != CURLMSG_DONE) { return; } auto curl = msg->easy_handle; @@ -225,13 +256,8 @@ public: } static std::unique_ptr create() { - auto curl = curl_easy_init(); - if (curl == nullptr) { - throw std::runtime_error("could not initialize cURL"); - } auto multiHandle = curl_multi_init(); if (multiHandle == nullptr) { - curl_easy_cleanup(curl); throw std::runtime_error("could not initialize cURL-multi"); } return std::make_unique(multiHandle); From ef3a5cf278393781aab693d5036a3f4f9b299b44 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 14 Sep 2026 12:41:12 +0300 Subject: [PATCH 32/56] feat: http response as table --- res/scripts/classes.lua | 24 +++++++++++++----- src/logic/scripting/lua/libs/libnetwork.cpp | 28 ++++++++++++++++++--- 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/res/scripts/classes.lua b/res/scripts/classes.lua index b044aa478..a6348f23a 100644 --- a/res/scripts/classes.lua +++ b/res/scripts/classes.lua @@ -127,8 +127,12 @@ network.get = function(url, callback, errorCallback, headers) return request(url, { method = "GET", headers = headers, - on_response = callback, - on_error = errorCallback, + on_response = function(response) + callback(response.body) + end, + on_error = function(response) + errorCallback(response.status, response.body) + end, follow_location = true, }) end @@ -137,8 +141,12 @@ network.get_binary = function(url, callback, errorCallback, headers) return request(url, { method = "GET", headers = headers, - on_response = callback and (function (response) return callback(Bytearray(response)) end), - on_error = errorCallback, + on_response = callback and (function (response) + return callback(Bytearray(response.body)) + end), + on_error = function(response) + errorCallback(response.status, response.body) + end, follow_location = true, }) end @@ -150,8 +158,12 @@ network.post = function(url, body, callback, errorCallback, headers) "Content-Type: application/json" }, headers), body = body, - on_response = callback, - on_error = errorCallback, + on_response = function(response) + callback(response.body) + end, + on_error = function(response) + errorCallback(response.status, response.body) + end, }) end diff --git a/src/logic/scripting/lua/libs/libnetwork.cpp b/src/logic/scripting/lua/libs/libnetwork.cpp index 475158dea..2ccf25546 100644 --- a/src/logic/scripting/lua/libs/libnetwork.cpp +++ b/src/logic/scripting/lua/libs/libnetwork.cpp @@ -28,7 +28,7 @@ struct ResponseEventDto { int status; bool binary; int requestId; - std::vector bytes; + network::HttpResponse response; }; enum NetworkDatagramSide { @@ -122,7 +122,7 @@ static int l_request(lua::State* L, network::Network& network) { response.status, false, currentRequestId, - std::move(response.body)} + std::move(response)} )); }; @@ -493,11 +493,31 @@ static int l_pull_events(lua::State* L) { lua::pushinteger(L, dto.requestId); lua::rawseti(L, 3); + lua::createtable(L, 0, 2); + lua::pushinteger(L, dto.response.status); + lua::setfield(L, "status"); + if (dto.binary) { - lua::create_bytearray(L, dto.bytes.data(), dto.bytes.size()); + lua::create_bytearray( + L, dto.response.body.data(), dto.response.body.size() + ); } else { - lua::pushlstring(L, std::string_view(dto.bytes.data(), dto.bytes.size())); + lua::pushlstring( + L, + std::string_view( + dto.response.body.data(), dto.response.body.size() + ) + ); } + lua::setfield(L, "body"); + + lua::createtable(L, dto.response.headers.size(), 0); + for (int i = 0; i < dto.response.headers.size(); i++) { + lua::pushlstring(L, dto.response.headers[i]); + lua::rawseti(L, i + 1); + } + lua::setfield(L, "headers"); + lua::rawseti(L, 4); break; } From afc8ff2a0fca8718fcdfb0e1681dfd3cb088de4a Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 14 Sep 2026 18:36:18 +0300 Subject: [PATCH 33/56] fix http error event --- res/scripts/classes.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/scripts/classes.lua b/res/scripts/classes.lua index a6348f23a..8c116139d 100644 --- a/res/scripts/classes.lua +++ b/res/scripts/classes.lua @@ -288,7 +288,7 @@ network.__process_events = function() _http_response_callbacks[event[3]] = nil _http_error_callbacks[event[3]] = nil if callback then - callback(event[2], event[4]) + callback(event[4]) end end end From 762634ea0494c3b8b0a9b8137f22215c7b7aeb69 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 14 Sep 2026 19:44:41 +0300 Subject: [PATCH 34/56] add verify_ssl parameter --- src/logic/scripting/lua/libs/libnetwork.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/logic/scripting/lua/libs/libnetwork.cpp b/src/logic/scripting/lua/libs/libnetwork.cpp index 2ccf25546..fbd722b1d 100644 --- a/src/logic/scripting/lua/libs/libnetwork.cpp +++ b/src/logic/scripting/lua/libs/libnetwork.cpp @@ -113,6 +113,10 @@ static int l_request(lua::State* L, network::Network& network) { request.timeoutMs = lua::tointeger(L, -1); lua::pop(L); } + if (lua::getfield(L, "verify_ssl", 2)) { + request.verifySSL = lua::toboolean(L, -1); + lua::pop(L); + } int currentRequestId = request_id++; request.onResponse = [currentRequestId](network::HttpResponse response) { From dad67db83500fcc373373936a1fafa26fdc78f3a Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 14 Sep 2026 19:51:05 +0300 Subject: [PATCH 35/56] feat: Bytearray request body support --- src/logic/scripting/lua/libs/libnetwork.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/logic/scripting/lua/libs/libnetwork.cpp b/src/logic/scripting/lua/libs/libnetwork.cpp index fbd722b1d..59d50f075 100644 --- a/src/logic/scripting/lua/libs/libnetwork.cpp +++ b/src/logic/scripting/lua/libs/libnetwork.cpp @@ -102,7 +102,11 @@ static int l_request(lua::State* L, network::Network& network) { lua::pop(L); } if (lua::getfield(L, "body", 2)) { - request.body = lua::require_lstring(L, -1); + if (lua::type(L, -1) == LUA_TCDATA) { + request.body = lua::bytearray_as_string(L, -1); + } else { + request.body = lua::require_lstring(L, -1); + } lua::pop(L); } if (lua::getfield(L, "follow_location", 2)) { From a773dfbd0e5da30411739e63b0ee0f60585dd558 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 14 Sep 2026 19:56:34 +0300 Subject: [PATCH 36/56] remove _http_error_callbacks --- res/scripts/classes.lua | 47 ++++++++++++++++++----------------------- 1 file changed, 20 insertions(+), 27 deletions(-) diff --git a/res/scripts/classes.lua b/res/scripts/classes.lua index 8c116139d..4a8c8915b 100644 --- a/res/scripts/classes.lua +++ b/res/scripts/classes.lua @@ -98,7 +98,6 @@ local _udp_server_callbacks = {} local _udp_client_datagram_callbacks = {} local _udp_client_open_callbacks = {} local _http_response_callbacks = {} -local _http_error_callbacks = {} local http_request = network.__request local open_tcp = network.__open_tcp @@ -116,9 +115,6 @@ local function request(url, params) if params.on_response then _http_response_callbacks[id] = params.on_response end - if params.on_error then - _http_error_callbacks[id] = params.on_error - end end network.request = request @@ -128,7 +124,11 @@ network.get = function(url, callback, errorCallback, headers) method = "GET", headers = headers, on_response = function(response) - callback(response.body) + if response.status / 100 == 2 then + return callback(response.body) + else + return errorCallback(response.status, response.body) + end end, on_error = function(response) errorCallback(response.status, response.body) @@ -142,11 +142,12 @@ network.get_binary = function(url, callback, errorCallback, headers) method = "GET", headers = headers, on_response = callback and (function (response) - return callback(Bytearray(response.body)) + if response.code / 100 == 2 then + return callback(Bytearray(response.body)) + else + return errorCallback(response.status, response.body) + end end), - on_error = function(response) - errorCallback(response.status, response.body) - end, follow_location = true, }) end @@ -159,11 +160,13 @@ network.post = function(url, body, callback, errorCallback, headers) }, headers), body = body, on_response = function(response) - callback(response.body) - end, - on_error = function(response) - errorCallback(response.status, response.body) + if response.code / 100 == 2 then + return callback(Bytearray(response.body)) + else + return errorCallback(response.status, response.body) + end end, + follow_location = true, }) end @@ -276,20 +279,10 @@ network.__process_events = function() end end elseif etype == RESPONSE then - if event[2] / 100 == 2 then - local callback = _http_response_callbacks[event[3]] - _http_response_callbacks[event[3]] = nil - _http_error_callbacks[event[3]] = nil - if callback then - callback(event[4]) - end - else - local callback = _http_error_callbacks[event[3]] - _http_response_callbacks[event[3]] = nil - _http_error_callbacks[event[3]] = nil - if callback then - callback(event[4]) - end + local callback = _http_response_callbacks[event[3]] + _http_response_callbacks[event[3]] = nil + if callback then + callback(event[4]) end end From 817092fef45f663adba6dff48a65aaaaf87eaa8e Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 14 Sep 2026 20:06:09 +0300 Subject: [PATCH 37/56] update doc/*/scripting/builtins/libnetwork.md --- doc/en/scripting/builtins/libnetwork.md | 33 +++++++++++++++++++++++ doc/ru/scripting/builtins/libnetwork.md | 35 ++++++++++++++++++++++++- 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/doc/en/scripting/builtins/libnetwork.md b/doc/en/scripting/builtins/libnetwork.md index 0ce5c1b0e..f0a330c3a 100644 --- a/doc/en/scripting/builtins/libnetwork.md +++ b/doc/en/scripting/builtins/libnetwork.md @@ -4,6 +4,39 @@ A library for working with the network. ## HTTP requests +There is a configurable function `network.request` that allows performing HTTP requests with various methods (GET, POST, PUT, DELETE, etc.) and configuring headers, request body, timeout, and other parameters. + +>[!NOTE] To pass binary data in the request body, use a byte array (Bytearray) or a string. In `on_response`, the `body` string can be converted to a byte array using `Bytearray(response.body)`. + +```lua +network.request( + url: string, + parameters: { + -- Request method (GET, POST, PUT, DELETE, etc.) + method: string, + -- Request body as a string or Bytearray + body: table|string, + -- List of additional request headers + headers: table, + -- Timeout in milliseconds + timeout: int, + -- Whether to verify the SSL certificate + verify_ssl: boolean, + -- Function called when a response is received + on_response: function(response: { + -- HTTP response status code + status: int, + -- Response body as a string + body: string, + -- List of response headers + headers: table + }), + } +) +``` + +### Simplified functions for GET and POST requests + ```lua -- Performs a GET request to the specified URL. network.get( diff --git a/doc/ru/scripting/builtins/libnetwork.md b/doc/ru/scripting/builtins/libnetwork.md index 98c4e51e0..c76a056a4 100644 --- a/doc/ru/scripting/builtins/libnetwork.md +++ b/doc/ru/scripting/builtins/libnetwork.md @@ -4,6 +4,39 @@ ## HTTP-Запросы +Существует настраиваемая функция `network.request`, которая позволяет выполнять HTTP-запросы с различными методами (GET, POST, PUT, DELETE и т.д.) и настраивать заголовки, тело запроса, таймаут и другие параметры. + +>[!NOTE] Для передачи двоичных данных в теле запроса, используйте массив байт (Bytearray) или строку. В `on_response` строку `body` можно преобразовать в массив байт с помощью `Bytearray(response.body)`. + +```lua +network.request( + url: string, + parameters: { + -- Метод запроса (GET, POST, PUT, DELETE и т.д.) + method: string, + -- Тело запроса в виде строки или массива байт (Bytearray) + body: table|string, + -- Список дополнительных заголовков запроса + headers: table, + -- Таймаут в миллисекундах + timeout: int, + -- Проверять ли SSL-сертификат + verify_ssl: boolean, + -- Функция, вызываемая при получении ответа + on_response: function(response: { + -- HTTP-код ответа + status: int, + -- Тело ответа в виде строки + body: string, + -- Список заголовков ответа + headers: table + }), + } +) +``` + +### Упрощённые функции для GET и POST запросов + ```lua -- Выполняет GET запрос к указанному URL. network.get( @@ -231,4 +264,4 @@ network.get_total_download() -> int ```lua -- Ищет свободный для использования порт. network.find_free_port() -> int | nil -``` \ No newline at end of file +``` From 38da5c89f532ec6aca6f70c460950da6ecc3b02d Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 14 Sep 2026 20:07:34 +0300 Subject: [PATCH 38/56] fix --- doc/en/scripting/builtins/libnetwork.md | 2 +- doc/ru/scripting/builtins/libnetwork.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/en/scripting/builtins/libnetwork.md b/doc/en/scripting/builtins/libnetwork.md index f0a330c3a..8893407d4 100644 --- a/doc/en/scripting/builtins/libnetwork.md +++ b/doc/en/scripting/builtins/libnetwork.md @@ -6,7 +6,7 @@ A library for working with the network. There is a configurable function `network.request` that allows performing HTTP requests with various methods (GET, POST, PUT, DELETE, etc.) and configuring headers, request body, timeout, and other parameters. ->[!NOTE] To pass binary data in the request body, use a byte array (Bytearray) or a string. In `on_response`, the `body` string can be converted to a byte array using `Bytearray(response.body)`. +> To pass binary data in the request body, use a byte array (Bytearray) or a string. In `on_response`, the `body` string can be converted to a byte array using `Bytearray(response.body)`. ```lua network.request( diff --git a/doc/ru/scripting/builtins/libnetwork.md b/doc/ru/scripting/builtins/libnetwork.md index c76a056a4..c60f0f178 100644 --- a/doc/ru/scripting/builtins/libnetwork.md +++ b/doc/ru/scripting/builtins/libnetwork.md @@ -6,7 +6,7 @@ Существует настраиваемая функция `network.request`, которая позволяет выполнять HTTP-запросы с различными методами (GET, POST, PUT, DELETE и т.д.) и настраивать заголовки, тело запроса, таймаут и другие параметры. ->[!NOTE] Для передачи двоичных данных в теле запроса, используйте массив байт (Bytearray) или строку. В `on_response` строку `body` можно преобразовать в массив байт с помощью `Bytearray(response.body)`. +> Для передачи двоичных данных в теле запроса, используйте массив байт (Bytearray) или строку. В `on_response` строку `body` можно преобразовать в массив байт с помощью `Bytearray(response.body)`. ```lua network.request( From 7958e82a25a853caab17757c844f8acd05b045e8 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 14 Sep 2026 22:16:18 +0300 Subject: [PATCH 39/56] add '--debugger' argument to vctest --- .github/workflows/appimage.yml | 2 +- .github/workflows/macos.yml | 2 +- vctest/main.cpp | 23 +++++++++++++++++------ 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/.github/workflows/appimage.yml b/.github/workflows/appimage.yml index 8d05f59b3..0d7670934 100644 --- a/.github/workflows/appimage.yml +++ b/.github/workflows/appimage.yml @@ -52,7 +52,7 @@ jobs: run: | chmod +x build/VoxelEngine chmod +x AppDir/usr/bin/vctest - AppDir/usr/bin/vctest -e build/VoxelEngine -d dev/tests -u build + AppDir/usr/bin/vctest -e build/VoxelEngine -d dev/tests -u build --memchecker valgrind - name: Build AppImage uses: AppImageCrafters/build-appimage-action@fe2205a4d6056be47051f7b1b3811106e9814910 env: diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 2bcb56dda..b4d9f6328 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -49,7 +49,7 @@ jobs: run: | chmod +x build/VoxelEngine chmod +x AppDir/usr/bin/vctest - AppDir/usr/bin/vctest -e build/VoxelEngine -d dev/tests -u build --output-always --memchecker "lldb -batch -o 'run' -o 'thread backtrace all' -o 'quit' -- " + AppDir/usr/bin/vctest -e build/VoxelEngine -d dev/tests -u build --output-always --debugger "lldb -batch -o 'run' -o 'thread backtrace all' -o 'quit' -- " - name: Create DMG run: | mkdir VoxelEngineDmgContent diff --git a/vctest/main.cpp b/vctest/main.cpp index 953d59aa9..99eebaacd 100644 --- a/vctest/main.cpp +++ b/vctest/main.cpp @@ -16,7 +16,8 @@ struct Config { fs::path directory; fs::path resDir {"res"}; fs::path workingDir {"."}; - std::string memchecker = "valgrind"; + std::string memchecker = ""; + std::string debugger = ""; bool outputAlways = false; }; @@ -30,6 +31,7 @@ static bool perform_keyword( std::cout << " --tests , -d = tests directory path\n"; std::cout << " --res , -r = 'res' directory path\n"; std::cout << " --user , -u = user directory path\n"; + std::cout << " --debugger = debugger with run command\n"; std::cout << " --memchecker = path to valgrind\n"; std::cout << " --output-always = always show tests output\n"; std::cout << std::endl; @@ -44,6 +46,8 @@ static bool perform_keyword( config.workingDir = fs::path(reader.next()); } else if (keyword == "--output-always") { config.outputAlways = true; + } else if (keyword == "--debugger") { + config.debugger = reader.next(); } else if (keyword == "--memchecker") { config.memchecker = reader.next(); } else { @@ -172,7 +176,7 @@ static std::string fix_path(std::string s) { return s; } -static bool run_test(const Config& config, const fs::path& path, bool memcheck = false) { +static bool run_test(const Config& config, const fs::path& path, bool debugRun = false) { using std::chrono::duration_cast; using std::chrono::high_resolution_clock; using std::chrono::milliseconds; @@ -182,9 +186,16 @@ static bool run_test(const Config& config, const fs::path& path, bool memcheck = auto name = path.stem(); std::stringstream ss; - if (memcheck) { - ss << config.memchecker << /*" --log-file=" - << fix_path(memcheckLogFile.string()) <<*/ " "; + if (debugRun) { + if (!config.memchecker.empty()) { + ss << config.memchecker; + ss << " --log-file=" << fix_path(memcheckLogFile.string()); + ss << " "; + } + if (!config.debugger.empty()) { + ss << config.debugger; + ss << " "; + } } ss << fs::canonical(config.executable) << " --headless"; ss << " --test " << fix_path(path.string()); @@ -203,7 +214,7 @@ static bool run_test(const Config& config, const fs::path& path, bool memcheck = .count(); if (code) { - if (memcheck) { + if (debugRun) { // valgrind-specific output display_segfault_valgrind(memcheckLogFile, name, std::cerr); fs::remove(memcheckLogFile); From 8d279466a90998e62be461311dd5e354d5101be9 Mon Sep 17 00:00:00 2001 From: dagger Date: Tue, 15 Sep 2026 04:45:07 +0700 Subject: [PATCH 40/56] move crypto Lua test to dev tests --- dev/tests/crypto.lua | 41 +++++++++++++++++++++++ test/crypto/CryptoLua.cpp | 70 --------------------------------------- 2 files changed, 41 insertions(+), 70 deletions(-) create mode 100644 dev/tests/crypto.lua delete mode 100644 test/crypto/CryptoLua.cpp diff --git a/dev/tests/crypto.lua b/dev/tests/crypto.lua new file mode 100644 index 000000000..96b36ecd9 --- /dev/null +++ b/dev/tests/crypto.lua @@ -0,0 +1,41 @@ +assert(crypto.API_VERSION == 1) +local features = crypto.features() +assert(features.api_version == 1) +assert(features.backend == "OpenSSL") + +local binary = "a\0b" +assert(crypto.hash("SHA256", binary) == crypto.sha256(binary)) +local stream = crypto.hash_new("SHA256") +assert(stream:update("a"):update("\0b"):final() == crypto.sha256(binary)) +stream:reset():update("abc") +assert(stream:final() == crypto.sha256("abc")) + +local private, public = crypto.ed25519_keypair() +assert(crypto.ed25519_public(private) == public) +local signature = crypto.ed25519_sign(private, binary) +assert(crypto.ed25519_verify(public, binary, signature) == true) +local ok, error_code = crypto.ed25519_verify(public, "changed", signature) +assert(ok == false and error_code == "invalid_signature") + +local x_private_a, x_public_a = crypto.x25519_keypair() +local x_private_b, x_public_b = crypto.x25519_keypair() +assert(crypto.x25519(x_private_a, x_public_b) == + crypto.x25519_shared(x_private_b, x_public_a)) + +local p_private_a, p_public_a = crypto.p256_keypair() +local p_private_b, p_public_b = crypto.p256_keypair() +assert(crypto.p256_public(p_private_a) == p_public_a) +assert(crypto.p256_shared(p_private_a, p_public_b) == + crypto.p256_shared(p_private_b, p_public_a)) + +local key = crypto.random_bytes(16) +local nonce = crypto.random_bytes(12) +local ciphertext = crypto.aes_gcm_encrypt(key, nonce, "aad", binary) +assert(crypto.aes_gcm_decrypt(key, nonce, "aad", ciphertext) == binary) +local plaintext, decrypt_error = crypto.aes_gcm_decrypt( + key, nonce, "changed", ciphertext +) +assert(plaintext == nil and decrypt_error == "authentication_failed") + +assert(#crypto.pbkdf2("SHA256", "password", "salt", 1, 32) == 32) +assert(#crypto.scrypt("password", "salt", 16, 1, 1, 32) == 32) diff --git a/test/crypto/CryptoLua.cpp b/test/crypto/CryptoLua.cpp deleted file mode 100644 index 3bce6af04..000000000 --- a/test/crypto/CryptoLua.cpp +++ /dev/null @@ -1,70 +0,0 @@ -#include - -#include - -#include "logic/scripting/lua/libs/api_lua.hpp" - -namespace { - struct LuaCloser { - void operator()(lua::State* state) const { - lua_close(state); - } - }; -} - -TEST(CryptoLua, PublicV1ApiWorksFromLua) { - std::unique_ptr state(luaL_newstate()); - ASSERT_NE(state, nullptr); - luaL_openlibs(state.get()); - lua::openlib(state.get(), "crypto", cryptolib); - initialize_cryptolib(state.get()); - - const char* script = R"lua( - assert(crypto.API_VERSION == 1) - local features = crypto.features() - assert(features.api_version == 1) - assert(features.backend == "OpenSSL") - - local binary = "a\0b" - assert(crypto.hash("SHA256", binary) == crypto.sha256(binary)) - local stream = crypto.hash_new("SHA256") - assert(stream:update("a"):update("\0b"):final() == crypto.sha256(binary)) - stream:reset():update("abc") - assert(stream:final() == crypto.sha256("abc")) - - local private, public = crypto.ed25519_keypair() - assert(crypto.ed25519_public(private) == public) - local signature = crypto.ed25519_sign(private, binary) - assert(crypto.ed25519_verify(public, binary, signature) == true) - local ok, error_code = crypto.ed25519_verify(public, "changed", signature) - assert(ok == false and error_code == "invalid_signature") - - local x_private_a, x_public_a = crypto.x25519_keypair() - local x_private_b, x_public_b = crypto.x25519_keypair() - assert(crypto.x25519(x_private_a, x_public_b) == - crypto.x25519_shared(x_private_b, x_public_a)) - - local p_private_a, p_public_a = crypto.p256_keypair() - local p_private_b, p_public_b = crypto.p256_keypair() - assert(crypto.p256_public(p_private_a) == p_public_a) - assert(crypto.p256_shared(p_private_a, p_public_b) == - crypto.p256_shared(p_private_b, p_public_a)) - - local key = crypto.random_bytes(16) - local nonce = crypto.random_bytes(12) - local ciphertext = crypto.aes_gcm_encrypt(key, nonce, "aad", binary) - assert(crypto.aes_gcm_decrypt(key, nonce, "aad", ciphertext) == binary) - local plaintext, decrypt_error = crypto.aes_gcm_decrypt( - key, nonce, "changed", ciphertext - ) - assert(plaintext == nil and decrypt_error == "authentication_failed") - - assert(#crypto.pbkdf2("SHA256", "password", "salt", 1, 32) == 32) - assert(#crypto.scrypt("password", "salt", 16, 1, 1, 32) == 32) - )lua"; - - const int result = luaL_dostring(state.get(), script); - if (result != 0) { - FAIL() << lua_tostring(state.get(), -1); - } -} From ae082abaa2f4a691951bc86a174ac0021f437566 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 16 Sep 2026 23:28:03 +0300 Subject: [PATCH 41/56] feat: allow passing rgba color to entity skeletons and block wraps --- res/shaders/entity.glslv | 6 ++-- src/animation/rigging.hpp | 2 +- src/graphics/render/BlockWrapsRenderer.cpp | 2 +- src/graphics/render/BlockWrapsRenderer.hpp | 4 +-- src/graphics/render/MainBatch.cpp | 2 +- src/graphics/render/MainBatch.hpp | 10 +++--- src/graphics/render/ModelBatch.cpp | 16 ++++++---- src/graphics/render/ModelBatch.hpp | 32 +++++++++++-------- src/graphics/render/ParticlesRenderer.cpp | 2 +- src/graphics/render/PrecipitationRenderer.cpp | 2 +- .../scripting/lua/libs/lib__skeleton.cpp | 4 ++- .../scripting/lua/libs/libblockwraps.cpp | 13 ++++++-- 12 files changed, 55 insertions(+), 40 deletions(-) diff --git a/res/shaders/entity.glslv b/res/shaders/entity.glslv index 8f534a6dc..579a15879 100644 --- a/res/shaders/entity.glslv +++ b/res/shaders/entity.glslv @@ -2,7 +2,7 @@ layout (location = 0) in vec3 v_position; layout (location = 1) in vec2 v_texCoord; -layout (location = 2) in vec3 v_color; +layout (location = 2) in vec4 v_color; layout (location = 3) in vec4 v_light; layout (location = 4) in vec4 v_normal; @@ -29,8 +29,8 @@ void main() { a_dir = a_modelpos.xyz - u_cameraPos; vec3 skyLightColor = pick_sky_color(u_skybox, u_dayTime, u_minSkyLight); - a_color.rgb = max(a_color.rgb, skyLightColor.rgb * v_light.a) * v_color; - a_color.a = u_opacity; + a_color.rgb = max(a_color.rgb, skyLightColor.rgb * v_light.a) * v_color.rgb; + a_color.a = u_opacity * v_color.a; mat4 viewmodel = u_view * u_model; a_distance = length(viewmodel * vec4(pos3d, 0.0)); diff --git a/src/animation/rigging.hpp b/src/animation/rigging.hpp index 82066e132..a2bd11375 100644 --- a/src/animation/rigging.hpp +++ b/src/animation/rigging.hpp @@ -85,7 +85,7 @@ namespace rigging { std::unordered_map textures; std::vector modelOverrides; bool visible; - glm::vec3 tint {1.0f, 1.0f, 1.0f}; + glm::vec4 tint {1.0f, 1.0f, 1.0f, 1.0f}; util::VecInterpolation<3, float> interpolation {false}; diff --git a/src/graphics/render/BlockWrapsRenderer.cpp b/src/graphics/render/BlockWrapsRenderer.cpp index 168ad14a2..7548afa34 100644 --- a/src/graphics/render/BlockWrapsRenderer.cpp +++ b/src/graphics/render/BlockWrapsRenderer.cpp @@ -161,7 +161,7 @@ void BlockWrapsRenderer::draw(const DrawContext& pctx) { u64id_t BlockWrapsRenderer::add( const glm::ivec3& position, const std::string& texture, - const glm::vec3& tint, + const glm::vec4& tint, float emission ) { u64id_t id = nextWrapper++; diff --git a/src/graphics/render/BlockWrapsRenderer.hpp b/src/graphics/render/BlockWrapsRenderer.hpp index 0f2ed8f7e..fb4893877 100644 --- a/src/graphics/render/BlockWrapsRenderer.hpp +++ b/src/graphics/render/BlockWrapsRenderer.hpp @@ -20,7 +20,7 @@ class DrawContext; struct BlockWrapper { glm::ivec3 position; std::array textureFaces {}; - std::array tints {}; + std::array tints {}; float emission = 0.0f; // --- render cache --- @@ -56,7 +56,7 @@ public: u64id_t add( const glm::ivec3& position, const std::string& texture, - const glm::vec3& tint, + const glm::vec4& tint, float emission ); diff --git a/src/graphics/render/MainBatch.cpp b/src/graphics/render/MainBatch.cpp index 91364e510..0bb6900dd 100644 --- a/src/graphics/render/MainBatch.cpp +++ b/src/graphics/render/MainBatch.cpp @@ -87,7 +87,7 @@ void MainBatch::cube( const glm::vec3& size, const UVRegion(&texfaces)[6], const glm::vec4& lights, - const glm::vec3 tints[], + const glm::vec4 tints[], float emission, uint8_t cullingBits ) { diff --git a/src/graphics/render/MainBatch.hpp b/src/graphics/render/MainBatch.hpp index 8ad119199..86397f36f 100644 --- a/src/graphics/render/MainBatch.hpp +++ b/src/graphics/render/MainBatch.hpp @@ -17,14 +17,14 @@ class Chunks; struct MainBatchVertex { glm::vec3 position; glm::vec2 uv; - glm::vec3 tint; + glm::vec4 tint; std::array color; std::array normal; static constexpr VertexAttribute ATTRIBUTES[] = { {VertexAttribute::Type::FLOAT, false, 3}, {VertexAttribute::Type::FLOAT, false, 2}, - {VertexAttribute::Type::FLOAT, false, 3}, + {VertexAttribute::Type::FLOAT, false, 4}, {VertexAttribute::Type::UNSIGNED_BYTE, true, 4}, {VertexAttribute::Type::UNSIGNED_BYTE, true, 4}, {{}, 0}}; @@ -63,7 +63,7 @@ public: const glm::vec3& pos, const glm::vec2& uv, const glm::vec4& light, - const glm::vec3& tint, + const glm::vec4& tint, const glm::vec3& normal, float emission ) { @@ -91,7 +91,7 @@ public: const glm::vec3& normal, const glm::vec2& size, const glm::vec4& light, - const glm::vec3& tint, + const glm::vec4& tint, const UVRegion& subregion, float emission = 0.0f ) { @@ -152,7 +152,7 @@ public: const glm::vec3& size, const UVRegion(&texfaces)[6], const glm::vec4& lights, - const glm::vec3 tints[], + const glm::vec4 tints[], float emission, uint8_t cullingBits = 0xFF ); diff --git a/src/graphics/render/ModelBatch.cpp b/src/graphics/render/ModelBatch.cpp index 35e891aff..949edfee0 100644 --- a/src/graphics/render/ModelBatch.cpp +++ b/src/graphics/render/ModelBatch.cpp @@ -61,8 +61,8 @@ void ModelBatch::draw( const model::Mesh& mesh, const glm::mat4& matrix, const glm::mat3& rotation, - glm::vec3 tint, - const texture_names_map* varTextures, + const glm::vec4& tint, + const TextureNamesMap* varTextures, bool backlight ) { setTexture(mesh.texture, varTextures); @@ -97,10 +97,12 @@ void ModelBatch::draw( } } -void ModelBatch::draw(glm::mat4 matrix, - glm::vec3 tint, - const model::Model* model, - const texture_names_map* varTextures) { +void ModelBatch::draw( + const glm::mat4& matrix, + const glm::vec4& tint, + const model::Model* model, + const TextureNamesMap* varTextures +) { for (const auto& mesh : model->meshes) { entries.push_back({ matrix, extract_rotation(matrix), tint, &mesh, varTextures @@ -134,7 +136,7 @@ void ModelBatch::setLightsOffset(const glm::vec3& offset) { } void ModelBatch::setTexture(const std::string& name, - const texture_names_map* varTextures) { + const TextureNamesMap* varTextures) { if (varTextures && !name.empty() && name.at(0) == '$') { const auto& found = varTextures->find(name); if (found == varTextures->end()) { diff --git a/src/graphics/render/ModelBatch.hpp b/src/graphics/render/ModelBatch.hpp index e93237865..460b8e13b 100644 --- a/src/graphics/render/ModelBatch.hpp +++ b/src/graphics/render/ModelBatch.hpp @@ -18,7 +18,7 @@ namespace model { struct Model; } -using texture_names_map = std::unordered_map; +using TextureNamesMap = std::unordered_map; class ModelBatch { const Assets& assets; @@ -31,22 +31,24 @@ class ModelBatch { std::unique_ptr batch; - void draw(const model::Mesh& mesh, - const glm::mat4& matrix, - const glm::mat3& rotation, - glm::vec3 tint, - const texture_names_map* varTextures, - bool backlight); + void draw( + const model::Mesh& mesh, + const glm::mat4& matrix, + const glm::mat3& rotation, + const glm::vec4& tint, + const TextureNamesMap* varTextures, + bool backlight + ); void setTexture(const std::string& name, - const texture_names_map* varTextures); + const TextureNamesMap* varTextures); struct DrawEntry { glm::mat4 matrix; glm::mat3 rotation; - glm::vec3 tint; + glm::vec4 tint; const model::Mesh* mesh; - const texture_names_map* varTextures; + const TextureNamesMap* varTextures; }; std::vector entries; public: @@ -58,10 +60,12 @@ public: ); ~ModelBatch(); - void draw(glm::mat4 matrix, - glm::vec3 tint, - const model::Model* model, - const texture_names_map* varTextures); + void draw( + const glm::mat4& matrix, + const glm::vec4& tint, + const model::Model* model, + const TextureNamesMap* varTextures + ); void render(); void setLightsOffset(const glm::vec3& offset); diff --git a/src/graphics/render/ParticlesRenderer.cpp b/src/graphics/render/ParticlesRenderer.cpp index d588cd63a..ea449479c 100644 --- a/src/graphics/render/ParticlesRenderer.cpp +++ b/src/graphics/render/ParticlesRenderer.cpp @@ -160,7 +160,7 @@ void ParticlesRenderer::renderParticle( -camera.front, preset.size * scale, light, - glm::vec3(1.0f), + glm::vec4(1.0f), particle.region, preset.lighting ? 0.0f : 1.0f ); diff --git a/src/graphics/render/PrecipitationRenderer.cpp b/src/graphics/render/PrecipitationRenderer.cpp index c566a06d2..6b2735d54 100644 --- a/src/graphics/render/PrecipitationRenderer.cpp +++ b/src/graphics/render/PrecipitationRenderer.cpp @@ -151,7 +151,7 @@ void PrecipitationRenderer::render( glm::cross(glm::vec3(0, 1, 0), face.right), FACE_SIZE, light_at(chunks, pos.x, y, pos.z), - glm::vec3(1.0f), + glm::vec4(1.0f), calc_uv(pos, face.right, timer, weather) ); } diff --git a/src/logic/scripting/lua/libs/lib__skeleton.cpp b/src/logic/scripting/lua/libs/lib__skeleton.cpp index a310abe87..fc673db77 100644 --- a/src/logic/scripting/lua/libs/lib__skeleton.cpp +++ b/src/logic/scripting/lua/libs/lib__skeleton.cpp @@ -156,7 +156,9 @@ static int l_get_color(lua::State* L) { static int l_set_color(lua::State* L) { if (auto skeleton = get_skeleton(L)) { - skeleton->tint = lua::tovec3(L, 2); + skeleton->tint = lua::objlen(L, 2) > 3 + ? lua::tovec4(L, 2) + : glm::vec4(lua::tovec3(L, 2), 1.0f); } return 0; } diff --git a/src/logic/scripting/lua/libs/libblockwraps.cpp b/src/logic/scripting/lua/libs/libblockwraps.cpp index 084dd6b8b..a6eba3e8f 100644 --- a/src/logic/scripting/lua/libs/libblockwraps.cpp +++ b/src/logic/scripting/lua/libs/libblockwraps.cpp @@ -10,7 +10,11 @@ static int l_wrap(lua::State* L) { auto position = lua::tovec3(L, 1); std::string texture = lua::require_string(L, 2); float emission = lua::isnumber(L, 3) ? lua::tonumber(L, 3) : 1.0f; - glm::vec3 tint = lua::istable(L, 4) ? lua::tovec3(L, 4) : glm::vec3(1.0f); + glm::vec4 tint = + lua::istable(L, 4) + ? (lua::objlen(L, 4) > 3 ? lua::tovec4(L, 4) + : glm::vec4(lua::tovec3(L, 4), 1.0f)) + : glm::vec4(1.0f); return lua::pushinteger( L, @@ -72,9 +76,12 @@ static int l_set_tints(lua::State* L) { if (auto wrapper = renderer->blockWraps->get(lua::tointeger(L, 1))) { for (int i = 0; i < wrapper->textureFaces.size(); i++) { if (lua::isnil(L, 2 + i)) { - wrapper->tints[i] = glm::vec3(1.0f); + wrapper->tints[i] = glm::vec4(1.0f); } else { - wrapper->tints[i] = lua::tovec3(L, 2 + i); + wrapper->tints[i] = + lua::objlen(L, 2 + i) > 3 + ? lua::tovec4(L, 2 + i) + : glm::vec4(lua::tovec3(L, 2 + i), 1.0f); } } } From dbeeef90a82a25b4bc261932ae283e1dd085fd03 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 16 Sep 2026 23:31:37 +0300 Subject: [PATCH 42/56] cleanup a bit --- src/graphics/render/MainBatch.cpp | 63 +++++++++++++++++++++++++ src/graphics/render/MainBatch.hpp | 76 +++++------------------------- src/graphics/render/ModelBatch.hpp | 37 ++++++++------- 3 files changed, 95 insertions(+), 81 deletions(-) diff --git a/src/graphics/render/MainBatch.cpp b/src/graphics/render/MainBatch.cpp index 0bb6900dd..70d4af6f7 100644 --- a/src/graphics/render/MainBatch.cpp +++ b/src/graphics/render/MainBatch.cpp @@ -82,6 +82,69 @@ inline glm::vec4 do_tint(float value) { return {value, value, value, 1.0f}; } +void MainBatch::quad( + const glm::vec3& pos, + const glm::vec3& right, + const glm::vec3& up, + const glm::vec3& normal, + const glm::vec2& size, + const glm::vec4& light, + const glm::vec4& tint, + const UVRegion& subregion, + float emission +) { + prepare(6); + vertex( + pos - right * size.x * 0.5f - up * size.y * 0.5f, + {subregion.u1, subregion.v1}, + light, + tint, + normal, + emission + ); + vertex( + pos + right * size.x * 0.5f - up * size.y * 0.5f, + {subregion.u2, subregion.v1}, + light, + tint, + normal, + emission + ); + vertex( + pos + right * size.x * 0.5f + up * size.y * 0.5f, + {subregion.u2, subregion.v2}, + light, + tint, + normal, + emission + ); + + vertex( + pos - right * size.x * 0.5f - up * size.y * 0.5f, + {subregion.u1, subregion.v1}, + light, + tint, + normal, + emission + ); + vertex( + pos + right * size.x * 0.5f + up * size.y * 0.5f, + {subregion.u2, subregion.v2}, + light, + tint, + normal, + emission + ); + vertex( + pos - right * size.x * 0.5f + up * size.y * 0.5f, + {subregion.u1, subregion.v2}, + light, + tint, + normal, + emission + ); +} + void MainBatch::cube( const glm::vec3& coord, const glm::vec3& size, diff --git a/src/graphics/render/MainBatch.hpp b/src/graphics/render/MainBatch.hpp index 86397f36f..d51b2d05b 100644 --- a/src/graphics/render/MainBatch.hpp +++ b/src/graphics/render/MainBatch.hpp @@ -31,16 +31,6 @@ struct MainBatchVertex { }; class MainBatch { - std::unique_ptr const buffer; - size_t const capacity; - size_t index; - - UVRegion region {0.0f, 0.0f, 1.0f, 1.0f}; - - std::unique_ptr> mesh; - std::unique_ptr blank; - - const Texture* texture = nullptr; public: /// xyz, uv, color, compressed lights @@ -84,7 +74,7 @@ public: index++; } - inline void quad( + void quad( const glm::vec3& pos, const glm::vec3& right, const glm::vec3& up, @@ -94,58 +84,7 @@ public: const glm::vec4& tint, const UVRegion& subregion, float emission = 0.0f - ) { - prepare(6); - vertex( - pos - right * size.x * 0.5f - up * size.y * 0.5f, - {subregion.u1, subregion.v1}, - light, - tint, - normal, - emission - ); - vertex( - pos + right * size.x * 0.5f - up * size.y * 0.5f, - {subregion.u2, subregion.v1}, - light, - tint, - normal, - emission - ); - vertex( - pos + right * size.x * 0.5f + up * size.y * 0.5f, - {subregion.u2, subregion.v2}, - light, - tint, - normal, - emission - ); - - vertex( - pos - right * size.x * 0.5f - up * size.y * 0.5f, - {subregion.u1, subregion.v1}, - light, - tint, - normal, - emission - ); - vertex( - pos + right * size.x * 0.5f + up * size.y * 0.5f, - {subregion.u2, subregion.v2}, - light, - tint, - normal, - emission - ); - vertex( - pos - right * size.x * 0.5f + up * size.y * 0.5f, - {subregion.u1, subregion.v2}, - light, - tint, - normal, - emission - ); - } + ); void cube( const glm::vec3& coord, @@ -156,4 +95,15 @@ public: float emission, uint8_t cullingBits = 0xFF ); +private: + std::unique_ptr const buffer; + size_t const capacity; + size_t index; + + UVRegion region {0.0f, 0.0f, 1.0f, 1.0f}; + + std::unique_ptr> mesh; + std::unique_ptr blank; + + const Texture* texture = nullptr; }; diff --git a/src/graphics/render/ModelBatch.hpp b/src/graphics/render/ModelBatch.hpp index 460b8e13b..84b182c83 100644 --- a/src/graphics/render/ModelBatch.hpp +++ b/src/graphics/render/ModelBatch.hpp @@ -21,6 +21,25 @@ namespace model { using TextureNamesMap = std::unordered_map; class ModelBatch { +public: + ModelBatch( + size_t capacity, + const Assets& assets, + const Chunks& chunks, + const EngineSettings& settings + ); + ~ModelBatch(); + + void draw( + const glm::mat4& matrix, + const glm::vec4& tint, + const model::Model* model, + const TextureNamesMap* varTextures + ); + void render(); + + void setLightsOffset(const glm::vec3& offset); +private: const Assets& assets; const Chunks& chunks; @@ -51,22 +70,4 @@ class ModelBatch { const TextureNamesMap* varTextures; }; std::vector entries; -public: - ModelBatch( - size_t capacity, - const Assets& assets, - const Chunks& chunks, - const EngineSettings& settings - ); - ~ModelBatch(); - - void draw( - const glm::mat4& matrix, - const glm::vec4& tint, - const model::Model* model, - const TextureNamesMap* varTextures - ); - void render(); - - void setLightsOffset(const glm::vec3& offset); }; From f5f17b7746e724f46cd4844b46ed7ef8f2610aa9 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 16 Sep 2026 23:37:44 +0300 Subject: [PATCH 43/56] feat: individual bone tints --- src/animation/rigging.cpp | 6 ++++-- src/animation/rigging.hpp | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/animation/rigging.cpp b/src/animation/rigging.cpp index d5e36a82c..c1d7a9e27 100644 --- a/src/animation/rigging.cpp +++ b/src/animation/rigging.cpp @@ -46,7 +46,8 @@ Skeleton::Skeleton(std::shared_ptr config) flags(config ? config->getBones().size() : 0), textures(), modelOverrides(config ? config->getBones().size() : 0), - visible(true) { + visible(true), + boneTints(config ? config->getBones().size() : 0, glm::vec4(1.0f)) { if (config == nullptr) { return; } @@ -99,6 +100,7 @@ void Skeleton::setConfig(std::shared_ptr rigConfig) { modelOverrides.resize(bonesCount); flags.resize(bonesCount); + boneTints.resize(bonesCount, glm::vec4(1.0f)); for (size_t i = 0; i < bonesCount; i++) { flags[i].visible = true; @@ -204,7 +206,7 @@ void SkeletonConfig::render( if (model) { batch.draw( skeleton.calculated.matrices[i], - skeleton.tint, + skeleton.tint * skeleton.boneTints[i], model, &skeleton.textures ); diff --git a/src/animation/rigging.hpp b/src/animation/rigging.hpp index a2bd11375..55d7fd8a7 100644 --- a/src/animation/rigging.hpp +++ b/src/animation/rigging.hpp @@ -86,6 +86,7 @@ namespace rigging { std::vector modelOverrides; bool visible; glm::vec4 tint {1.0f, 1.0f, 1.0f, 1.0f}; + std::vector boneTints; util::VecInterpolation<3, float> interpolation {false}; From ad22763a1ffef533a964de384723c5fc75ec0a0b Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 16 Sep 2026 23:40:41 +0300 Subject: [PATCH 44/56] add new skeleton:get_color, skeleton:set_color overloads --- res/modules/internal/stdcomp.lua | 4 ++-- .../scripting/lua/libs/lib__skeleton.cpp | 20 ++++++++++++++++--- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/res/modules/internal/stdcomp.lua b/res/modules/internal/stdcomp.lua index 4a0ace852..dfea2e7c9 100644 --- a/res/modules/internal/stdcomp.lua +++ b/res/modules/internal/stdcomp.lua @@ -58,8 +58,8 @@ local Skeleton = {__index={ index=function(self, s) return __skeleton.index(self.eid, s) end, is_visible=function(self, i) return __skeleton.is_visible(self.eid, i) end, set_visible=function(self, i, b) return __skeleton.set_visible(self.eid, i, b) end, - get_color=function(self) return __skeleton.get_color(self.eid) end, - set_color=function(self, color) return __skeleton.set_color(self.eid, color) end, + get_color=function(self, ...) return __skeleton.get_color(self.eid, ...) end, + set_color=function(self, color, ...) return __skeleton.set_color(self.eid, color, ...) end, set_interpolated=function(self, b) return __skeleton.set_interpolated(self.eid, b) end, }} diff --git a/src/logic/scripting/lua/libs/lib__skeleton.cpp b/src/logic/scripting/lua/libs/lib__skeleton.cpp index fc673db77..136f4ce62 100644 --- a/src/logic/scripting/lua/libs/lib__skeleton.cpp +++ b/src/logic/scripting/lua/libs/lib__skeleton.cpp @@ -147,8 +147,19 @@ static int l_set_visible(lua::State* L) { return 0; } +static glm::vec4 to_color(lua::State* L, int idx) { + if (lua::objlen(L, idx) > 3) { + return lua::tovec4(L, idx); + } + return glm::vec4(lua::tovec3(L, idx), 1.0f); +} + static int l_get_color(lua::State* L) { if (auto skeleton = get_skeleton(L)) { + if (lua::isnumber(L, 2)) { + auto index = index_range_check(*skeleton, lua::tointeger(L, 2)); + return lua::pushvec(L, skeleton->boneTints[index]); + } return lua::pushvec(L, skeleton->tint); } return 0; @@ -156,9 +167,12 @@ static int l_get_color(lua::State* L) { static int l_set_color(lua::State* L) { if (auto skeleton = get_skeleton(L)) { - skeleton->tint = lua::objlen(L, 2) > 3 - ? lua::tovec4(L, 2) - : glm::vec4(lua::tovec3(L, 2), 1.0f); + if (lua::isnumber(L, 2)) { + auto index = index_range_check(*skeleton, lua::tointeger(L, 2)); + skeleton->boneTints[index] = to_color(L, 3); + } else { + skeleton->tint = to_color(L, 2); + } } return 0; } From f130be4c42a97397390a5d4ca3f0ad01c22cfd77 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 16 Sep 2026 23:43:24 +0300 Subject: [PATCH 45/56] update doc/*/scripting/ecs.md --- doc/en/scripting/ecs.md | 8 ++++++-- doc/ru/scripting/ecs.md | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/doc/en/scripting/ecs.md b/doc/en/scripting/ecs.md index d9194ea93..ddc361998 100644 --- a/doc/en/scripting/ecs.md +++ b/doc/en/scripting/ecs.md @@ -190,10 +190,14 @@ rig:is_visible([optional] index: int) -> bool rig:set_visible([optional] index: int, status: bool) -- Returns the color of the entity -rig:get_color() -> vec3 +rig:get_color() -> vec4 +-- Return the color of the bone by index +rig:get_color(index: int) -> vec4 -- Sets the color of the entity -rig:set_color(color: vec3) +rig:set_color(color: vec3 | vec4) +-- Sets the color of the bone by index +rig:set_color(index: int, color: vec3 | vec4) ``` > [!WARNING] diff --git a/doc/ru/scripting/ecs.md b/doc/ru/scripting/ecs.md index fa6186d41..3c50ef26b 100644 --- a/doc/ru/scripting/ecs.md +++ b/doc/ru/scripting/ecs.md @@ -179,10 +179,14 @@ rig:is_visible([optional] index: int) -> bool rig:set_visible([optional] index: int, status: bool) -- Возвращает цвет сущности -rig:get_color() -> vec3 +rig:get_color() -> vec4 +-- Возвращает цвет кости по индексу +rig:get_color(index: int) -> vec4 -- Устанавливает цвет сущности -rig:set_color(color: vec3) +rig:set_color(color: vec3 | vec4) +-- Устанавливает цвет кости по индексу +rig:set_color(index: int, color: vec3 | vec4) ``` ## Пользовательские компоненты From 3e2f311e672be9d592cc94ed0f2c63ffa3d547da Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 16 Sep 2026 23:47:25 +0300 Subject: [PATCH 46/56] update methods schemes --- doc/en/scripting/ecs.md | 2 +- doc/ru/scripting/ecs.md | 2 +- src/logic/scripting/lua/libs/lib__skeleton.cpp | 9 +++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/doc/en/scripting/ecs.md b/doc/en/scripting/ecs.md index ddc361998..0473fc852 100644 --- a/doc/en/scripting/ecs.md +++ b/doc/en/scripting/ecs.md @@ -197,7 +197,7 @@ rig:get_color(index: int) -> vec4 -- Sets the color of the entity rig:set_color(color: vec3 | vec4) -- Sets the color of the bone by index -rig:set_color(index: int, color: vec3 | vec4) +rig:set_color(color: vec3 | vec4, index: int) ``` > [!WARNING] diff --git a/doc/ru/scripting/ecs.md b/doc/ru/scripting/ecs.md index 3c50ef26b..d6a823cf0 100644 --- a/doc/ru/scripting/ecs.md +++ b/doc/ru/scripting/ecs.md @@ -186,7 +186,7 @@ rig:get_color(index: int) -> vec4 -- Устанавливает цвет сущности rig:set_color(color: vec3 | vec4) -- Устанавливает цвет кости по индексу -rig:set_color(index: int, color: vec3 | vec4) +rig:set_color(color: vec3 | vec4, index: int) ``` ## Пользовательские компоненты diff --git a/src/logic/scripting/lua/libs/lib__skeleton.cpp b/src/logic/scripting/lua/libs/lib__skeleton.cpp index 136f4ce62..c1004434f 100644 --- a/src/logic/scripting/lua/libs/lib__skeleton.cpp +++ b/src/logic/scripting/lua/libs/lib__skeleton.cpp @@ -167,11 +167,12 @@ static int l_get_color(lua::State* L) { static int l_set_color(lua::State* L) { if (auto skeleton = get_skeleton(L)) { - if (lua::isnumber(L, 2)) { - auto index = index_range_check(*skeleton, lua::tointeger(L, 2)); - skeleton->boneTints[index] = to_color(L, 3); + auto color = to_color(L, 2); + if (lua::isnumber(L, 3)) { + auto index = index_range_check(*skeleton, lua::tointeger(L, 3)); + skeleton->boneTints[index] = color; } else { - skeleton->tint = to_color(L, 2); + skeleton->tint = color; } } return 0; From 5ae4c1b27b6293d346ef253939bbe2c6cf80af3b Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 17 Sep 2026 21:58:38 +0300 Subject: [PATCH 47/56] add gui.screenshot --- src/logic/scripting/lua/libs/libgui.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/logic/scripting/lua/libs/libgui.cpp b/src/logic/scripting/lua/libs/libgui.cpp index 81e5fc4f7..28033132f 100644 --- a/src/logic/scripting/lua/libs/libgui.cpp +++ b/src/logic/scripting/lua/libs/libgui.cpp @@ -25,6 +25,7 @@ #include "items/Inventories.hpp" #include "util/stringutil.hpp" #include "world/Level.hpp" +#include "window/Window.hpp" #include "../usertypes/lua_type_canvas.hpp" using namespace gui; @@ -1218,7 +1219,16 @@ static int l_get_active_frame(lua::State* L) { return lua::pushstring(L, frame->getId()); } +static int l_screenshot(lua::State* L) { + if (engine->isHeadless()) { + return 0; + } + auto image = engine->getWindow().takeScreenshot(); + return lua::newuserdata(L, nullptr, std::move(image)); +} + const luaL_Reg guilib[] = { + {"screenshot", lua::wrap}, {"get_viewport", lua::wrap}, {"getattr", lua::wrap}, {"setattr", lua::wrap}, From 69edfc8c8a1d98f79b7f5b8d05fdb0bb85063543 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 17 Sep 2026 22:07:18 +0300 Subject: [PATCH 48/56] add 'frame' argument to gui.screenshot --- src/graphics/core/Framebuffer.cpp | 7 +++++++ src/graphics/core/Framebuffer.hpp | 3 +++ src/graphics/ui/GUI.cpp | 8 ++++++++ src/graphics/ui/GUI.hpp | 2 ++ src/graphics/ui/elements/Frame.cpp | 7 +++++++ src/graphics/ui/elements/Frame.hpp | 3 +++ src/logic/scripting/lua/libs/libgui.cpp | 12 +++++++++++- 7 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/graphics/core/Framebuffer.cpp b/src/graphics/core/Framebuffer.cpp index 1dc677096..a10da01ad 100644 --- a/src/graphics/core/Framebuffer.cpp +++ b/src/graphics/core/Framebuffer.cpp @@ -117,6 +117,13 @@ std::shared_ptr Framebuffer::getSharedTexture() const { return texture; } +std::unique_ptr Framebuffer::readData() const { + if (!texture) { + return nullptr; + } + return texture->readData(); +} + uint Framebuffer::getWidth() const { return width; } diff --git a/src/graphics/core/Framebuffer.hpp b/src/graphics/core/Framebuffer.hpp index 5f9dc3491..00c1422a7 100644 --- a/src/graphics/core/Framebuffer.hpp +++ b/src/graphics/core/Framebuffer.hpp @@ -6,6 +6,7 @@ #include class Texture; +class ImageData; class Framebuffer : public Bindable { uint fbo; @@ -36,6 +37,8 @@ public: std::shared_ptr getSharedTexture() const; + std::unique_ptr readData() const; + /// @brief Get framebuffer width uint getWidth() const; /// @brief Get framebuffer height diff --git a/src/graphics/ui/GUI.cpp b/src/graphics/ui/GUI.cpp index 944c32fb5..a5f3010ff 100644 --- a/src/graphics/ui/GUI.cpp +++ b/src/graphics/ui/GUI.cpp @@ -378,6 +378,14 @@ std::shared_ptr GUI::getActiveFrame() const { return activeFrame; } +std::shared_ptr GUI::getFrame(const std::string& id) { + const auto& found = frames.find(id); + if (found == frames.end()) { + return nullptr; + } + return found->second; +} + void GUI::remove(UINode* node) noexcept { container->remove(node); } diff --git a/src/graphics/ui/GUI.hpp b/src/graphics/ui/GUI.hpp index 97fc0ed37..997b67bea 100644 --- a/src/graphics/ui/GUI.hpp +++ b/src/graphics/ui/GUI.hpp @@ -137,6 +137,8 @@ namespace gui { std::shared_ptr getActiveFrame() const; + std::shared_ptr getFrame(const std::string& id); + /// @brief Remove node from the main container void remove(UINode* node) noexcept; diff --git a/src/graphics/ui/elements/Frame.cpp b/src/graphics/ui/elements/Frame.cpp index 0387f5c1e..fe3562b4f 100644 --- a/src/graphics/ui/elements/Frame.cpp +++ b/src/graphics/ui/elements/Frame.cpp @@ -66,3 +66,10 @@ const std::string& gui::Frame::getOutputTexture() const { const std::string& gui::Frame::getFrameId() const { return frameId; } + +std::unique_ptr gui::Frame::takeScreenshot() const { + if (fbo == nullptr) { + return nullptr; + } + return fbo->readData(); +} diff --git a/src/graphics/ui/elements/Frame.hpp b/src/graphics/ui/elements/Frame.hpp index 1baa7bc91..f7513324b 100644 --- a/src/graphics/ui/elements/Frame.hpp +++ b/src/graphics/ui/elements/Frame.hpp @@ -4,6 +4,7 @@ class Framebuffer; class UiDocument; +class ImageData; namespace gui { class Frame final : public Container { @@ -18,6 +19,8 @@ namespace gui { const std::string& getOutputTexture() const; const std::string& getFrameId() const; + + std::unique_ptr takeScreenshot() const; private: std::string frameId; std::unique_ptr fbo; diff --git a/src/logic/scripting/lua/libs/libgui.cpp b/src/logic/scripting/lua/libs/libgui.cpp index 28033132f..389bedc9a 100644 --- a/src/logic/scripting/lua/libs/libgui.cpp +++ b/src/logic/scripting/lua/libs/libgui.cpp @@ -1223,7 +1223,17 @@ static int l_screenshot(lua::State* L) { if (engine->isHeadless()) { return 0; } - auto image = engine->getWindow().takeScreenshot(); + std::unique_ptr image; + if (lua::isstring(L, 1)) { + auto& gui = engine->getGUI(); + auto frame = gui.getFrame(lua::require_string(L, 1)); + if (frame == nullptr) { + return 0; + } + image = frame->takeScreenshot(); + } else { + image = engine->getWindow().takeScreenshot(); + } return lua::newuserdata(L, nullptr, std::move(image)); } From 71368836c85ce446222e38ea861d4a023c8f8461 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 17 Sep 2026 22:12:11 +0300 Subject: [PATCH 49/56] update doc/*/scripting/builtins/libgui.md --- doc/en/scripting/builtins/libgui.md | 6 ++++++ doc/ru/scripting/builtins/libgui.md | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/doc/en/scripting/builtins/libgui.md b/doc/en/scripting/builtins/libgui.md index a7ddbeaec..3dc10612b 100644 --- a/doc/en/scripting/builtins/libgui.md +++ b/doc/en/scripting/builtins/libgui.md @@ -61,6 +61,12 @@ gui.set_active_frame( -- Used for custom projection (e.g., in 3D) [optional] cursorLocator: function() -> number, number ) + +-- Creates a screenshot of a frame as a Canvas object if the frame ID is specified, or the entire window if nil. +gui.screenshot( + -- ID of the frame created via gui.create_frame + [optional] frameId: str +) -> Canvas | nil ``` ## Markup diff --git a/doc/ru/scripting/builtins/libgui.md b/doc/ru/scripting/builtins/libgui.md index ea1221415..51ac7307f 100644 --- a/doc/ru/scripting/builtins/libgui.md +++ b/doc/ru/scripting/builtins/libgui.md @@ -69,6 +69,12 @@ gui.set_active_frame( -- Используется для пользовательской проекции (например в 3D) [опционально] cursorLocator: function() -> number, number ) + +-- Создаёт снимок фрейма в виде объекта Canvas если указан id фрейма, или всего окна, в случае nil. +gui.screenshot( + -- id фрейма, созданного через gui.create_frame + [опционально] frameId: str +) -> Canvas | nil ``` ## Разметка From ca9ff7ba7e5afe97fc8aa1af57c86e93c99fa9c3 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sat, 19 Sep 2026 15:27:21 +0300 Subject: [PATCH 50/56] fix missing default rotation-order value --- res/modules/internal/formats/vca.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/modules/internal/formats/vca.lua b/res/modules/internal/formats/vca.lua index fc910a8d7..f06c99a7b 100644 --- a/res/modules/internal/formats/vca.lua +++ b/res/modules/internal/formats/vca.lua @@ -24,7 +24,7 @@ local function parse_configure(raw_track, node) elseif node.duration then raw_track.duration = node.duration end - raw_track.rotation_order = string.upper(node["rotation-order"]) + raw_track.rotation_order = string.upper(node["rotation-order"] or "XYZ") end local function parse_curve(line, node) From 8911cded07036f6c23684f4d6abe03a39afa3f29 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sat, 19 Sep 2026 18:13:15 +0300 Subject: [PATCH 51/56] add 'lighting-mode' entity property --- src/animation/rigging.cpp | 29 ++++++++++++++++++--------- src/animation/rigging.hpp | 12 +++++++++++ src/content/loading/EntityLoader.cpp | 4 ++++ src/graphics/render/HandsRenderer.cpp | 1 + src/graphics/render/ModelBatch.cpp | 13 +++++++++--- src/graphics/render/ModelBatch.hpp | 2 ++ src/objects/Entities.cpp | 9 ++++++++- src/objects/EntityDef.cpp | 1 + src/objects/EntityDef.hpp | 8 ++++---- 9 files changed, 61 insertions(+), 18 deletions(-) diff --git a/src/animation/rigging.cpp b/src/animation/rigging.cpp index c1d7a9e27..7a4061f8a 100644 --- a/src/animation/rigging.cpp +++ b/src/animation/rigging.cpp @@ -177,6 +177,7 @@ void SkeletonConfig::render( const Assets& assets, ModelBatch& batch, Skeleton& skeleton, + ModelLightingMode lightingMode, const glm::mat3& rotation, const glm::vec3& position, const glm::vec3& scale @@ -203,16 +204,24 @@ void SkeletonConfig::render( if (auto foundOverride = modelOverride.model.lock()) { model = foundOverride.get(); } - if (model) { - batch.draw( - skeleton.calculated.matrices[i], - skeleton.tint * skeleton.boneTints[i], - model, - &skeleton.textures - ); - } else if (!node->model.name.empty()) { - node->model.updateFlag = true; - } + if (model == nullptr) { + if (!node->model.name.empty()) { + node->model.updateFlag = true; + } + continue; + } + batch.draw( + skeleton.calculated.matrices[i], + skeleton.tint * skeleton.boneTints[i], + lightingMode == ModelLightingMode::SOLID + ? position - glm::vec3( + skeleton.calculated.matrices[i] * + glm::vec4(0.0f, 0.0f, 0.0f, 1.0f) + ) + : glm::vec3(), + model, + &skeleton.textures + ); } } diff --git a/src/animation/rigging.hpp b/src/animation/rigging.hpp index 55d7fd8a7..35d7fd01b 100644 --- a/src/animation/rigging.hpp +++ b/src/animation/rigging.hpp @@ -11,6 +11,7 @@ #include "typedefs.hpp" #include "data/dv_fwd.hpp" #include "util/Interpolation.hpp" +#include "util/EnumMetadata.hpp" class Assets; class ModelBatch; @@ -19,6 +20,16 @@ namespace model { struct Model; } +enum class ModelLightingMode { + BONE_BASED, + SOLID, +}; + +VC_ENUM_METADATA(ModelLightingMode) + {"bone-based", ModelLightingMode::BONE_BASED}, + {"solid", ModelLightingMode::SOLID}, +VC_ENUM_END + namespace rigging { struct Skeleton; class SkeletonConfig; @@ -132,6 +143,7 @@ namespace rigging { const Assets& assets, ModelBatch& batch, Skeleton& skeleton, + ModelLightingMode lightingMode, const glm::mat3& rotation, const glm::vec3& position, const glm::vec3& scale diff --git a/src/content/loading/EntityLoader.cpp b/src/content/loading/EntityLoader.cpp index e785fb0fe..625b1927a 100644 --- a/src/content/loading/EntityLoader.cpp +++ b/src/content/loading/EntityLoader.cpp @@ -94,4 +94,8 @@ template<> void ContentUnitLoader::loadUnit( root.at("mass").get(def.mass); root.at("elasticity").get(def.elasticity); root.at("step-height").get(def.stepHeight); + + std::string lightingModeName; + root.at("lighting-mode").get(lightingModeName); + ModelLightingModeMeta.getItem(lightingModeName, def.lightingMode); } diff --git a/src/graphics/render/HandsRenderer.cpp b/src/graphics/render/HandsRenderer.cpp index 3fc21342a..27e43a078 100644 --- a/src/graphics/render/HandsRenderer.cpp +++ b/src/graphics/render/HandsRenderer.cpp @@ -30,6 +30,7 @@ void HandsRenderer::render(const Camera& camera) { assets, modelBatch, skeleton, + ModelLightingMode::SOLID, glm::mat3(1.0f), glm::vec3(), glm::vec3(1.0f) diff --git a/src/graphics/render/ModelBatch.cpp b/src/graphics/render/ModelBatch.cpp index 949edfee0..3eed7b507 100644 --- a/src/graphics/render/ModelBatch.cpp +++ b/src/graphics/render/ModelBatch.cpp @@ -100,13 +100,18 @@ void ModelBatch::draw( void ModelBatch::draw( const glm::mat4& matrix, const glm::vec4& tint, + const glm::vec3& lightSampleOffset, const model::Model* model, const TextureNamesMap* varTextures ) { for (const auto& mesh : model->meshes) { - entries.push_back({ - matrix, extract_rotation(matrix), tint, &mesh, varTextures - }); + entries.push_back(DrawEntry { + matrix, + extract_rotation(matrix), + tint, + lightSampleOffset, + &mesh, + varTextures}); } } @@ -118,6 +123,7 @@ void ModelBatch::render() { ); bool backlight = settings.graphics.backlight.get(); for (auto& entry : entries) { + setLightsOffset(entry.lightSampleOffset); draw( *entry.mesh, entry.matrix, @@ -126,6 +132,7 @@ void ModelBatch::render() { entry.varTextures, backlight ); + setLightsOffset(glm::vec3()); } batch->flush(); entries.clear(); diff --git a/src/graphics/render/ModelBatch.hpp b/src/graphics/render/ModelBatch.hpp index 84b182c83..4a1dfbc56 100644 --- a/src/graphics/render/ModelBatch.hpp +++ b/src/graphics/render/ModelBatch.hpp @@ -33,6 +33,7 @@ public: void draw( const glm::mat4& matrix, const glm::vec4& tint, + const glm::vec3& lightSampleOffset, const model::Model* model, const TextureNamesMap* varTextures ); @@ -66,6 +67,7 @@ private: glm::mat4 matrix; glm::mat3 rotation; glm::vec4 tint; + glm::vec3 lightSampleOffset; const model::Mesh* mesh; const TextureNamesMap* varTextures; }; diff --git a/src/objects/Entities.cpp b/src/objects/Entities.cpp index 1158c68a0..c2ce479c6 100644 --- a/src/objects/Entities.cpp +++ b/src/objects/Entities.cpp @@ -482,6 +482,7 @@ void Entities::render( if (eid.uid == fpsEntity) { continue; } + const auto& def = eid.def; const auto& pos = transform.pos; const auto& size = transform.size; if (frustum && !frustum->isBoxVisible(pos - size, pos + size)) { @@ -491,7 +492,13 @@ void Entities::render( const auto& rigConfig = skeleton.config; if (rigConfig) { rigConfig->render( - assets, batch, skeleton, transform.rot, pos, size + assets, + batch, + skeleton, + def.lightingMode, + transform.rot, + pos, + size ); } } diff --git a/src/objects/EntityDef.cpp b/src/objects/EntityDef.cpp index ae1f87c68..54e0aefef 100644 --- a/src/objects/EntityDef.cpp +++ b/src/objects/EntityDef.cpp @@ -14,4 +14,5 @@ void EntityDef::cloneTo(EntityDef& dst) { dst.elasticity = elasticity; dst.stepHeight = stepHeight; dst.save = save; + dst.lightingMode = lightingMode; } diff --git a/src/objects/EntityDef.hpp b/src/objects/EntityDef.hpp index 59aed2d7e..b378c02e4 100644 --- a/src/objects/EntityDef.hpp +++ b/src/objects/EntityDef.hpp @@ -8,10 +8,7 @@ #include "data/dv.hpp" #include "maths/aabb.hpp" #include "physics/Hitbox.hpp" - -namespace rigging { - class SkeletonConfig; -} +#include "animation/rigging.hpp" struct ComponentInstance { std::string component; @@ -42,6 +39,9 @@ struct EntityDef { /// @brief Solid entity material material std::string material; + /// @brief Model lighting mode + ModelLightingMode lightingMode = ModelLightingMode::BONE_BASED; + /// @brief Can the entity be interacted with cursor (on_attack, on_use) bool selectable = true; From 600730d57e429c57ae27b66df60da5f384655621 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sat, 19 Sep 2026 18:55:20 +0300 Subject: [PATCH 52/56] update doc/*/entity-properties.md --- doc/en/entity-properties.md | 5 +++++ doc/ru/entity-properties.md | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/doc/en/entity-properties.md b/doc/en/entity-properties.md index e25c6d71d..c2c852c35 100644 --- a/doc/en/entity-properties.md +++ b/doc/en/entity-properties.md @@ -127,6 +127,11 @@ Defines the entity's material (same as for blocks). The default value is the same as the entity name. Determines which skeleton will be used by the entity. See [rigging](rigging.md). +### *lighting-mode* + +- *bone-based* - lighting is calculated for each skeletal bone separately. +- *solid* - lighting is calculated for the entire entity. + ## Saving/Loading In addition to custom components, the engine automatically saves data from the built-in ones: transform, rigidbody, skeleton. diff --git a/doc/ru/entity-properties.md b/doc/ru/entity-properties.md index 71dc6dc88..20ff752c7 100644 --- a/doc/ru/entity-properties.md +++ b/doc/ru/entity-properties.md @@ -127,6 +127,11 @@ Значение по-умолчанию совпадает с именем сущности. Определяет то, какой скелет будет использоваться сущностью. См. [риггинг](rigging.md). +### Тип освещения - *lighting-mode* + +- *bone-based* - освещение рассчитывается для каждой кости скелета отдельно. +- *solid* - освещение рассчитывается для всей сущности в целом. + ## Сохранение/загрузка Кроме данных пользовательских компонентов, движок автоматически сохраняет данные встроенных: transform, rigidbody, skeleton. From 6f78a15eface5eb403e955cf2ed315133c778549 Mon Sep 17 00:00:00 2001 From: alexei-zebra Date: Sat, 19 Sep 2026 19:11:11 +0300 Subject: [PATCH 53/56] fix: add include to Curl.cpp --- src/network/Curl.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/network/Curl.cpp b/src/network/Curl.cpp index 1c6507d36..0e08162fb 100644 --- a/src/network/Curl.cpp +++ b/src/network/Curl.cpp @@ -4,6 +4,7 @@ #define NOMINMAX #include +#include using namespace network; From 19f037a9fd144b02b8c8cc0886f0c91f8a85c986 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sat, 19 Sep 2026 19:30:29 +0300 Subject: [PATCH 54/56] make 'solid' lighting mode default --- src/objects/EntityDef.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/objects/EntityDef.hpp b/src/objects/EntityDef.hpp index b378c02e4..139bce48c 100644 --- a/src/objects/EntityDef.hpp +++ b/src/objects/EntityDef.hpp @@ -40,7 +40,7 @@ struct EntityDef { std::string material; /// @brief Model lighting mode - ModelLightingMode lightingMode = ModelLightingMode::BONE_BASED; + ModelLightingMode lightingMode = ModelLightingMode::SOLID; /// @brief Can the entity be interacted with cursor (on_attack, on_use) bool selectable = true; From e306624260565c9dee323c6ef12fa41192606d4f Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sat, 19 Sep 2026 19:41:11 +0300 Subject: [PATCH 55/56] fix hand items lighting --- src/graphics/render/ModelBatch.cpp | 6 +++--- src/graphics/render/ModelBatch.hpp | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/graphics/render/ModelBatch.cpp b/src/graphics/render/ModelBatch.cpp index 3eed7b507..35b5e6274 100644 --- a/src/graphics/render/ModelBatch.cpp +++ b/src/graphics/render/ModelBatch.cpp @@ -72,7 +72,7 @@ void ModelBatch::draw( glm::vec4 lights(1, 1, 1, 0); if (mesh.shading) { glm::vec3 gpos = matrix * glm::vec4(0.0f, 0.0f, 0.0f, 1.0f); - gpos += lightsOffset; + gpos += lightsOffset + localLightsOffset; lights = MainBatch::sampleLight(gpos, chunks, backlight); } for (size_t i = 0; i < vcount / 3; i++) { @@ -123,7 +123,7 @@ void ModelBatch::render() { ); bool backlight = settings.graphics.backlight.get(); for (auto& entry : entries) { - setLightsOffset(entry.lightSampleOffset); + localLightsOffset = entry.lightSampleOffset; draw( *entry.mesh, entry.matrix, @@ -132,7 +132,7 @@ void ModelBatch::render() { entry.varTextures, backlight ); - setLightsOffset(glm::vec3()); + localLightsOffset = {}; } batch->flush(); entries.clear(); diff --git a/src/graphics/render/ModelBatch.hpp b/src/graphics/render/ModelBatch.hpp index 4a1dfbc56..5d43024d5 100644 --- a/src/graphics/render/ModelBatch.hpp +++ b/src/graphics/render/ModelBatch.hpp @@ -46,6 +46,7 @@ private: const EngineSettings& settings; glm::vec3 lightsOffset {}; + glm::vec3 localLightsOffset {}; static inline glm::vec3 SUN_VECTOR {0.411934f, 0.863868f, -0.279161f}; From 56d2ab9f1466b781a45e17298c3b4907c70a40bc Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sat, 19 Sep 2026 20:04:30 +0300 Subject: [PATCH 56/56] fix custom model blocks with disabled ambient-occlusion --- src/graphics/render/BlocksRenderer.cpp | 34 +++++++++++++++++--------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/src/graphics/render/BlocksRenderer.cpp b/src/graphics/render/BlocksRenderer.cpp index f75bd675c..8b8a667eb 100644 --- a/src/graphics/render/BlocksRenderer.cpp +++ b/src/graphics/render/BlocksRenderer.cpp @@ -357,20 +357,30 @@ void BlocksRenderer::blockCustomModel( const auto& vcoord = vertex.coord - 0.5f; glm::vec4 aoColor {1.0f, 1.0f, 1.0f, 1.0f}; - if (shading && ao) { + if (shading) { const float eps = 0.05f; auto p = coord + vcoord.x * X + vcoord.y * Y + vcoord.z * Z + - r * 0.5f + t * 0.5f + n * eps; - auto p1 = p + n * eps; - auto p2 = p + n * 0.5f; - aoColor = pickSoftLight(p1.x, p1.y, p1.z, glm::ivec3(r), glm::ivec3(t)); - if (!block.lightPassing) { - aoColor = glm::max( - aoColor, - pickSoftLight( - p2.x, p2.y, p2.z, glm::ivec3(r), glm::ivec3(t) - ) - ); + r * 0.5f + t * 0.5f + n * eps; + if (ao) { + auto p1 = p + n * eps; + auto p2 = p + n * 0.5f; + aoColor = pickSoftLight(p1.x, p1.y, p1.z, glm::ivec3(r), glm::ivec3(t)); + if (!block.lightPassing) { + aoColor = glm::max( + aoColor, + pickSoftLight( + p2.x, p2.y, p2.z, glm::ivec3(r), glm::ivec3(t) + ) + ); + } + } else { + aoColor = pickLight(glm::floor(p + 0.5f) + 0.5f); + if (!block.lightPassing) { + aoColor = glm::max( + aoColor, + pickLight(glm::floor(p + n + 0.5f) + 0.5f) + ); + } } } this->vertex(