From a7ad219261fa1f5859c85f07ec74c1341254813a Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 12 May 2026 02:02:06 +0300 Subject: [PATCH 001/217] test environment (sky) --- src/data/dv.hpp | 28 ++++ src/graphics/core/Cubemap.cpp | 23 ++- src/graphics/core/Cubemap.hpp | 1 + src/graphics/core/Framebuffer.cpp | 60 ++++--- src/graphics/core/Framebuffer.hpp | 3 +- src/graphics/core/Texture.cpp | 20 ++- src/graphics/core/Texture.hpp | 6 +- src/graphics/render/Skybox.cpp | 219 +++++++++++++++++--------- src/graphics/render/Skybox.hpp | 52 +++--- src/graphics/render/WorldRenderer.cpp | 146 +++++++++-------- src/graphics/render/WorldRenderer.hpp | 2 + src/world/Environment.cpp | 25 +++ src/world/Environment.hpp | 30 ++++ src/world/Level.hpp | 2 + 14 files changed, 421 insertions(+), 196 deletions(-) create mode 100644 src/world/Environment.cpp create mode 100644 src/world/Environment.hpp diff --git a/src/data/dv.hpp b/src/data/dv.hpp index 3c3f4a71e..8acd22136 100644 --- a/src/data/dv.hpp +++ b/src/data/dv.hpp @@ -9,6 +9,10 @@ #include #include +#ifdef VC_ENABLE_REFLECTION +#include "util/EnumMetadata.hpp" +#endif + namespace util { template class Buffer; } @@ -70,6 +74,14 @@ namespace dv { return ptr != nullptr; } + inline value* operator->() noexcept { + return ptr; + } + + inline const value* operator->() const noexcept { + return ptr; + } + inline value& operator*() noexcept { return *ptr; } @@ -78,6 +90,11 @@ namespace dv { return *ptr; } +#ifdef VC_ENABLE_REFLECTION + template + bool get(T& dst, const util::EnumMetadata& mt) const; +#endif + bool get(std::string& dst) const; bool get(bool& dst) const; bool get(char& dst) const; @@ -552,6 +569,17 @@ namespace dv { } return false; } + +#ifdef VC_ENABLE_REFLECTION + template + inline bool optionalvalue::get(T& dst, const util::EnumMetadata& mt) const { + if (ptr) { + return mt.getItem(ptr->asString(), dst); + } + return false; + } +#endif + inline bool optionalvalue::get(std::string& dst) const { if (ptr) { dst = ptr->asString(); diff --git a/src/graphics/core/Cubemap.cpp b/src/graphics/core/Cubemap.cpp index 631a8d7e4..e55fb3127 100644 --- a/src/graphics/core/Cubemap.cpp +++ b/src/graphics/core/Cubemap.cpp @@ -4,7 +4,7 @@ #include Cubemap::Cubemap(uint width, uint height, ImageFormat imageFormat) - : Texture(0, width, height) + : Texture(0U, width, height, imageFormat) { glGenTextures(1, &id); glBindTexture(GL_TEXTURE_CUBE_MAP, id); @@ -14,7 +14,6 @@ Cubemap::Cubemap(uint width, uint height, ImageFormat imageFormat) glTexParameterf(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameterf(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); - uint format = gl::to_glenum(imageFormat); for (uint face = 0; face < 6; face++) { glTexImage2D( GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, @@ -37,3 +36,23 @@ void Cubemap::bind() const { void Cubemap::unbind() const { glBindTexture(GL_TEXTURE_CUBE_MAP, 0); } + +void Cubemap::reload(const ubyte* data, uint width, uint height) { + this->width = width; + this->height = height; + glBindTexture(GL_TEXTURE_CUBE_MAP, id); + for (uint face = 0; face < 6; face++) { + glTexImage2D( + GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, + 0, + format, + width, + height, + 0, + format, + GL_UNSIGNED_BYTE, + data + ); + } + glBindTexture(GL_TEXTURE_CUBE_MAP, 0); +} diff --git a/src/graphics/core/Cubemap.hpp b/src/graphics/core/Cubemap.hpp index 4ca1b9884..e266c4381 100644 --- a/src/graphics/core/Cubemap.hpp +++ b/src/graphics/core/Cubemap.hpp @@ -9,4 +9,5 @@ public: virtual void bind() const override; virtual void unbind() const override; + virtual void reload(const ubyte* data, uint w, uint h) override; }; diff --git a/src/graphics/core/Framebuffer.cpp b/src/graphics/core/Framebuffer.cpp index 5d6be967d..dde31f6be 100644 --- a/src/graphics/core/Framebuffer.cpp +++ b/src/graphics/core/Framebuffer.cpp @@ -1,14 +1,14 @@ #include "Framebuffer.hpp" #include + #include "Texture.hpp" #include "debug/Logger.hpp" static debug::Logger logger("gl-framebuffer"); Framebuffer::Framebuffer(uint fbo, uint depth, std::unique_ptr texture) - : fbo(fbo), depth(depth), texture(std::move(texture)) -{ + : fbo(fbo), depth(depth), texture(std::move(texture)) { if (this->texture) { width = this->texture->getWidth(); height = this->texture->getHeight(); @@ -18,35 +18,55 @@ Framebuffer::Framebuffer(uint fbo, uint depth, std::unique_ptr texture) } } -static std::unique_ptr create_texture(int width, int height, int format) { - GLuint tex; - glGenTextures(1, &tex); - glBindTexture(GL_TEXTURE_2D, tex); - glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, nullptr); +static std::unique_ptr create_texture( + int width, int height, bool alpha +) { + GLenum glformat = alpha ? GL_RGBA : GL_RGB; + + GLuint textureid; + glGenTextures(1, &textureid); + glBindTexture(GL_TEXTURE_2D, textureid); + glTexImage2D( + GL_TEXTURE_2D, + 0, + glformat, + width, + height, + 0, + glformat, + GL_UNSIGNED_BYTE, + nullptr + ); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0); - return std::make_unique(tex, width, height); + glFramebufferTexture2D( + GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textureid, 0 + ); + return std::make_unique( + textureid, + width, + height, + alpha ? ImageFormat::RGBA8888 : ImageFormat::RGB888 + ); } -Framebuffer::Framebuffer(uint width, uint height, bool alpha) - : width(width), height(height) -{ +Framebuffer::Framebuffer(uint width, uint height, bool alpha) + : width(width), height(height) { glGenFramebuffers(1, &fbo); glBindFramebuffer(GL_FRAMEBUFFER, fbo); - format = alpha ? GL_RGBA : GL_RGB; - // Setup color attachment (texture) - texture = create_texture(width, height, format); + texture = create_texture(width, height, alpha); // Setup depth attachment glGenRenderbuffers(1, &depth); glBindRenderbuffer(GL_RENDERBUFFER, depth); glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, width, height); - glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depth); + glFramebufferRenderbuffer( + GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depth + ); if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { logger.error() << "framebuffer is not complete!"; @@ -60,6 +80,10 @@ Framebuffer::~Framebuffer() { glDeleteRenderbuffers(1, &depth); } +void Framebuffer::setTexture(std::unique_ptr texture) { + this->texture = std::move(texture); +} + void Framebuffer::bind() { glBindFramebuffer(GL_FRAMEBUFFER, fbo); } @@ -81,7 +105,7 @@ void Framebuffer::resize(uint width, uint height) { glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, width, height); glBindRenderbuffer(GL_RENDERBUFFER, 0); - texture = create_texture(width, height, format); + texture->resize(width, height); glBindFramebuffer(GL_FRAMEBUFFER, 0); } @@ -102,5 +126,5 @@ uint Framebuffer::getHeight() const { } uint Framebuffer::getFBO() const { - return fbo; + return fbo; } diff --git a/src/graphics/core/Framebuffer.hpp b/src/graphics/core/Framebuffer.hpp index 4d21449c5..f64d33f37 100644 --- a/src/graphics/core/Framebuffer.hpp +++ b/src/graphics/core/Framebuffer.hpp @@ -12,13 +12,14 @@ class Framebuffer : public Bindable { uint depth; uint width; uint height; - uint format; std::shared_ptr texture; public: Framebuffer(uint fbo, uint depth, std::unique_ptr texture); Framebuffer(uint width, uint height, bool alpha=false); ~Framebuffer(); + void setTexture(std::unique_ptr texture); + /// @brief Use framebuffer void bind() override; diff --git a/src/graphics/core/Texture.cpp b/src/graphics/core/Texture.cpp index d60bd2779..5f326d20c 100644 --- a/src/graphics/core/Texture.cpp +++ b/src/graphics/core/Texture.cpp @@ -7,8 +7,8 @@ uint Texture::MAX_RESOLUTION = 1024; // Window.initialize overrides it -Texture::Texture(uint id, uint width, uint height) - : id(id), width(width), height(height) { +Texture::Texture(uint id, uint width, uint height, ImageFormat imageFormat) + : id(id), width(width), height(height), format(gl::to_glenum(imageFormat)) { } Texture::Texture(const ubyte* data, uint width, uint height, ImageFormat imageFormat) @@ -17,7 +17,7 @@ Texture::Texture(const ubyte* data, uint width, uint height, ImageFormat imageFo glBindTexture(GL_TEXTURE_2D, id); glPixelStorei(GL_UNPACK_ALIGNMENT, 1); - GLenum format = gl::to_glenum(imageFormat); + format = gl::to_glenum(imageFormat); glTexImage2D( GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, static_cast(data) @@ -41,23 +41,27 @@ void Texture::unbind() const { glBindTexture(GL_TEXTURE_2D, 0); } +void Texture::resize(uint width, uint height) { + reload(nullptr, width, height); +} + void Texture::reload(const ImageData& image) { - width = image.getWidth(); - height = image.getHeight(); reload(image.getData(), width, height); } void Texture::reload(const ubyte* data, uint width, uint height) { + this->width = width; + this->height = height; glBindTexture(GL_TEXTURE_2D, id); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, - GL_RGBA, GL_UNSIGNED_BYTE, static_cast(data)); + glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, + format, GL_UNSIGNED_BYTE, static_cast(data)); glGenerateMipmap(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, 0); } void Texture::reloadPartial(const ImageData& image, uint x, uint y, uint w, uint h) { glBindTexture(GL_TEXTURE_2D, id); - glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, w, h, GL_RGBA, GL_UNSIGNED_BYTE, image.getData()); + glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, w, h, format, GL_UNSIGNED_BYTE, image.getData()); glGenerateMipmap(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, 0); } diff --git a/src/graphics/core/Texture.hpp b/src/graphics/core/Texture.hpp index 3c8f1b4cb..50734a577 100644 --- a/src/graphics/core/Texture.hpp +++ b/src/graphics/core/Texture.hpp @@ -11,19 +11,21 @@ protected: uint id; uint width; uint height; + uint format; public: - Texture(uint id, uint width, uint height); + Texture(uint id, uint width, uint height, ImageFormat imageFormat); Texture(const ubyte* data, uint width, uint height, ImageFormat format); virtual ~Texture(); virtual void bind() const; virtual void unbind() const; - void reload(const ubyte* data, uint w, uint h); + virtual void reload(const ubyte* data, uint w, uint h); void reloadPartial(const ImageData& image, uint x, uint y, uint w, uint h); void setNearestFilter(); void reload(const ImageData& image); + virtual void resize(uint w, uint h); void setMipMapping(bool flag, bool pixelated); diff --git a/src/graphics/render/Skybox.cpp b/src/graphics/render/Skybox.cpp index c546c62e3..79e5552af 100644 --- a/src/graphics/render/Skybox.cpp +++ b/src/graphics/render/Skybox.cpp @@ -1,47 +1,44 @@ #include "Skybox.hpp" -#include "assets/Assets.hpp" -#include "graphics/core/Shader.hpp" -#include "graphics/core/Mesh.hpp" -#include "graphics/core/Batch3D.hpp" -#include "graphics/core/Texture.hpp" -#include "graphics/core/Cubemap.hpp" -#include "graphics/core/Framebuffer.hpp" -#include "graphics/core/DrawContext.hpp" -#include "window/Window.hpp" -#include "window/Camera.hpp" -#include "maths/UVRegion.hpp" + +#include #include -#include -#include #include -#include #include +#include +#include + +#include "assets/Assets.hpp" +#include "graphics/core/Batch3D.hpp" +#include "graphics/core/Cubemap.hpp" +#include "graphics/core/DrawContext.hpp" +#include "graphics/core/Framebuffer.hpp" +#include "graphics/core/Mesh.hpp" +#include "graphics/core/Shader.hpp" +#include "graphics/core/Texture.hpp" +#include "maths/UVRegion.hpp" +#include "window/Camera.hpp" +#include "window/Window.hpp" using namespace advanced_pipeline; const int STARS_COUNT = 3000; + const int STARS_SEED = 632; -Skybox::Skybox(uint size, Shader& shader) - : size(size), - shader(shader), - batch3d(std::make_unique(4096)) -{ - auto cubemap = std::make_unique(size, size, ImageFormat::RGB888); +Skybox::Skybox(uint size, const Assets& assets) + : size(size), + assets(assets), + shader(assets.require("skybox_gen")), + batch3d(std::make_unique(4096)) { - uint fboid; - glGenFramebuffers(1, &fboid); - fbo = std::make_unique(fboid, 0, std::move(cubemap)); - - SkyboxVertex vertices[]{ + SkyboxVertex vertices[] { {{-1.0f, -1.0f}}, {{-1.0f, 1.0f}}, {{1.0f, 1.0f}}, {{-1.0f, -1.0f}}, {{1.0f, 1.0f}}, - {{1.0f, -1.0f}} - }; + {{1.0f, -1.0f}}}; mesh = std::make_unique>(vertices, 6); @@ -52,7 +49,7 @@ Skybox::Skybox(uint size, Shader& shader) false, glm::pi() * 0.25f, }); - + sprites.push_back(SkySprite { "misc/moon_flare", glm::pi() * 0.5f, @@ -68,20 +65,20 @@ Skybox::Skybox(uint size, Shader& shader) true, glm::pi() * 0.25f, }); + + setMode(mode); } Skybox::~Skybox() = default; -void Skybox::drawBackground( - const Camera& camera, const Assets& assets, int width, int height -) { +void Skybox::drawBackground(const Camera& camera, int width, int height) { auto backShader = assets.get("background"); backShader->use(); backShader->uniformMatrix("u_view", camera.getView(false)); backShader->uniform1f( "u_zoom", camera.zoom * camera.getFov() / glm::half_pi() ); - backShader->uniform1f("u_ar", float(width)/float(height)); + backShader->uniform1f("u_ar", float(width) / float(height)); backShader->uniform1i("u_skybox", 1); bind(); mesh->draw(); @@ -93,9 +90,7 @@ void Skybox::drawStars(float angle, float opacity) { random.setSeed(STARS_SEED); glm::mat4 rotation = glm::rotate( - glm::mat4(1.0f), - -angle + glm::pi() * 0.5f, - glm::vec3(0, 0, -1) + glm::mat4(1.0f), -angle + glm::pi() * 0.5f, glm::vec3(0, 0, -1) ); rotation = glm::rotate(rotation, sunAltitude, glm::vec3(1, 0, 0)); @@ -108,41 +103,20 @@ void Skybox::drawStars(float angle, float opacity) { glm::vec3 pos = glm::vec4(rx, ry, rz, 1) * rotation; float sopacity = random.randFloat(); - if (pos.y < 0.0f) - continue; + if (pos.y < 0.0f) continue; sopacity *= (0.2f + std::sqrt(std::cos(angle)) * 0.5f) - 0.05f; - glm::vec4 tint (1,1,1, sopacity * opacity); + glm::vec4 tint(1, 1, 1, sopacity * opacity); batch3d->point(pos * depth, tint); } batch3d->flushPoints(); } -void Skybox::draw( - const DrawContext& pctx, - const Camera& camera, - const Assets& assets, +void Skybox::drawSkySprites( float daytime, - float fog) -{ - const glm::uvec2& viewport = pctx.getViewport(); - - glActiveTexture(GL_TEXTURE0); - - drawBackground(camera, assets, viewport.x, viewport.y); - - DrawContext ctx = pctx.sub(); - ctx.setBlendMode(BlendMode::addition); - - auto shader = assets.get("ui3d"); - shader->use(); - shader->uniformMatrix("u_projview", camera.getProjView(false)); - shader->uniformMatrix("u_apply", glm::mat4(1.0f)); - batch3d->begin(); - - float angle = daytime * glm::pi() * 2.0f; - float opacity = glm::pow(1.0f - fog, 7.0f); - + float angle, + float opacity +) { float depthScale = 2e3; for (auto& sprite : sprites) { batch3d->texture(assets.get(sprite.texture)); @@ -159,39 +133,132 @@ void Skybox::draw( glm::vec3 pos = glm::vec4(0, distance, 0, 1) * rotation; glm::vec3 up = glm::vec4(depthScale, 0, 0, 1) * rotation; glm::vec3 right = glm::vec4(0, 0, depthScale, 1) * rotation; - glm::vec4 tint (1,1,1, opacity); + glm::vec4 tint(1, 1, 1, opacity); if (!sprite.emissive) { - tint *= 0.6f + std::cos(angle)*0.4; + tint *= 0.6f + std::cos(angle) * 0.4; } - batch3d->sprite(pos, right, - up, 1, 1, UVRegion(), tint); + batch3d->sprite(pos, right, up, 1, 1, UVRegion(), tint); } batch3d->flush(); - drawStars(angle, opacity); } -void Skybox::refresh(const DrawContext& pctx, float t, float mie, const glm::vec3& tint, const glm::vec3& hightlight, uint quality) { +void Skybox::draw( + const Environment& environment, + const DrawContext& pctx, + const Camera& camera, + float daytime, + float fog +) { + const glm::uvec2& viewport = pctx.getViewport(); + + glActiveTexture(GL_TEXTURE0); + + drawBackground(camera, viewport.x, viewport.y); + + float angle = daytime * glm::pi() * 2.0f; + float opacity = glm::pow(1.0f - fog, 7.0f); + + DrawContext ctx = pctx.sub(); + ctx.setBlendMode(BlendMode::addition); + + if (!environment.sky.sprites && !environment.sky.stars) { + return; + } + + auto shader = assets.get("ui3d"); + shader->use(); + shader->uniformMatrix("u_projview", camera.getProjView(false)); + shader->uniformMatrix("u_apply", glm::mat4(1.0f)); + batch3d->begin(); + + if (environment.sky.sprites) { + drawSkySprites(daytime, angle, opacity); + } + if (environment.sky.stars) { + drawStars(angle, opacity); + } +} + +void Skybox::setMode(SkyMode mode) { + if (this->mode == mode && fbo) { + return; + } + this->mode = mode; + + uint size = mode == SkyMode::BOX ? this->size : 1U; + prevT = std::numeric_limits().min(); + + uint fboid; + glGenFramebuffers(1, &fboid); + fbo = std::make_unique( + fboid, + false, + std::make_unique(size, size, ImageFormat::RGB888) + ); +} + +void Skybox::refresh( + const Environment& environment, + const DrawContext& pctx, + float dayTime, + float mie, + const glm::vec3& tint, + const glm::vec3& hightlight, + uint quality +) { + setMode(environment.sky.mode); + + if (mode == SkyMode::NONE) { + return; + } else if (mode == SkyMode::SOLID) { + DrawContext ctx = pctx.sub(); + ctx.setDepthMask(false); + ctx.setDepthTest(false); + ctx.setFramebuffer(fbo.get()); + ctx.setViewport({fbo->getWidth(), fbo->getHeight()}); + + auto cubemap = dynamic_cast(fbo->getTexture()); + assert(cubemap != nullptr); + + glActiveTexture(GL_TEXTURE0 + TARGET_SKYBOX); + cubemap->bind(); + + for (int face = 0; face < 6; face++) { + glFramebufferTexture2D( + GL_FRAMEBUFFER, + GL_COLOR_ATTACHMENT0, + GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, + cubemap->getId(), + 0 + ); + glClearColor(tint.r, tint.g, tint.b, 1); + glClear(GL_COLOR_BUFFER_BIT); + } + + cubemap->unbind(); + return; + } + frameid++; - float dayTime = t; DrawContext ctx = pctx.sub(); ctx.setDepthMask(false); ctx.setDepthTest(false); ctx.setFramebuffer(fbo.get()); - ctx.setViewport({size, size}); + ctx.setViewport({fbo->getWidth(), fbo->getHeight()}); auto cubemap = dynamic_cast(fbo->getTexture()); assert(cubemap != nullptr); - ready = true; glActiveTexture(GL_TEXTURE0 + TARGET_SKYBOX); cubemap->bind(); shader.use(); - t *= glm::two_pi(); + float t = dayTime * glm::two_pi(); lightDir = glm::normalize(glm::vec3(sin(t), -cos(t), 0.0f)); - float sunAngle = glm::radians((t / glm::two_pi() - 0.25f) * 360.0f); - float x = -glm::cos(sunAngle + glm::pi() * 0.5f) * glm::radians(sunAltitude); + float sunAngle = glm::radians((dayTime - 0.25f) * 360.0f); + float x = -glm::cos(sunAngle + glm::pi() * 0.5f) * + glm::radians(sunAltitude); float y = sunAngle - glm::pi() * 0.5f; float z = glm::radians(0.0f); rotation = glm::rotate(glm::mat4(1.0f), y, glm::vec3(0, 1, 0)); @@ -207,7 +274,9 @@ void Skybox::refresh(const DrawContext& pctx, float t, float mie, const glm::vec shader.uniform3f("u_lightDir", lightDir); shader.uniform1f("u_dayTime", dayTime); - if (glm::abs(mie-prevMie) + glm::abs(t-prevT) + glm::abs(prevHighlight.r - hightlight.r) >= 0.01) { + if (glm::abs(mie - prevMie) + glm::abs(t - prevT) + + glm::abs(prevHighlight.r - hightlight.r) >= + 0.01) { for (uint face = 0; face < 6; face++) { refreshFace(face, cubemap); } diff --git a/src/graphics/render/Skybox.hpp b/src/graphics/render/Skybox.hpp index d2dde6f5e..270dc066d 100644 --- a/src/graphics/render/Skybox.hpp +++ b/src/graphics/render/Skybox.hpp @@ -1,14 +1,18 @@ #pragma once +#include #include #include #include -#include -#include "typedefs.hpp" -#include "maths/fastmaths.hpp" -#include "graphics/core/MeshData.hpp" -template class Mesh; +#include "graphics/core/MeshData.hpp" +#include "maths/fastmaths.hpp" +#include "typedefs.hpp" +#include "world/Environment.hpp" + + +template +class Mesh; class Shader; class Assets; class Camera; @@ -21,8 +25,7 @@ struct SkyboxVertex { glm::vec2 position; static constexpr VertexAttribute ATTRIBUTES[] { - {VertexAttribute::Type::FLOAT, false, 2}, - {{}, 0}}; + {VertexAttribute::Type::FLOAT, false, 2}, {{}, 0}}; }; struct SkySprite { @@ -34,10 +37,11 @@ struct SkySprite { }; class Skybox { - std::unique_ptr fbo; + SkyMode mode = SkyMode::SOLID; uint size; + std::unique_ptr fbo; + const Assets& assets; Shader& shader; - bool ready = false; FastRandom random; glm::vec3 lightDir; @@ -53,30 +57,36 @@ class Skybox { glm::mat4 rotation; void drawStars(float angle, float opacity); - void drawBackground( - const Camera& camera, const Assets& assets, int width, int height - ); + void drawBackground(const Camera& camera, int width, int height); + void drawSkySprites(float daytime, float angle, float opacity); void refreshFace(uint face, Cubemap* cubemap); public: - Skybox(uint size, Shader& shader); + Skybox(uint size, const Assets& assets); ~Skybox(); + void setMode(SkyMode mode); + void draw( - const DrawContext& pctx, - const Camera& camera, - const Assets& assets, + const Environment& environment, + const DrawContext& pctx, + const Camera& camera, float daytime, float fog ); - void refresh(const DrawContext& pctx, float t, float mie, const glm::vec3& tint, const glm::vec3& hightlight, uint quality); + void refresh( + const Environment& environment, + const DrawContext& pctx, + float t, + float mie, + const glm::vec3& tint, + const glm::vec3& hightlight, + uint quality + ); void bind() const; void unbind() const; - bool isReady() const { - return ready; - } - const glm::vec3 getLightDir() const { + const glm::vec3& getLightDir() const { return lightDir; } }; diff --git a/src/graphics/render/WorldRenderer.cpp b/src/graphics/render/WorldRenderer.cpp index 2e150e1f7..e5d838f7d 100644 --- a/src/graphics/render/WorldRenderer.cpp +++ b/src/graphics/render/WorldRenderer.cpp @@ -1,12 +1,45 @@ #include "WorldRenderer.hpp" +#include + +#include +#include +#include +#include + +#include "BlockWrapsRenderer.hpp" +#include "ChunksRenderer.hpp" +#include "CloudsRenderer.hpp" +#include "DebugLinesRenderer.hpp" +#include "Emitter.hpp" +#include "HandsRenderer.hpp" +#include "ModelBatch.hpp" +#include "NamedSkeletons.hpp" +#include "ParticlesRenderer.hpp" +#include "PrecipitationRenderer.hpp" +#include "Skybox.hpp" +#include "TextNote.hpp" +#include "TextsRenderer.hpp" #include "assets/Assets.hpp" #include "assets/assets_util.hpp" +#include "coders/GLSLExtension.hpp" #include "content/Content.hpp" #include "engine/Engine.hpp" -#include "coders/GLSLExtension.hpp" -#include "frontend/LevelFrontend.hpp" #include "frontend/ContentGfxCache.hpp" +#include "frontend/LevelFrontend.hpp" +#include "graphics/commons/Model.hpp" +#include "graphics/core/Atlas.hpp" +#include "graphics/core/Batch3D.hpp" +#include "graphics/core/DrawContext.hpp" +#include "graphics/core/Font.hpp" +#include "graphics/core/Framebuffer.hpp" +#include "graphics/core/GBuffer.hpp" +#include "graphics/core/LineBatch.hpp" +#include "graphics/core/Mesh.hpp" +#include "graphics/core/PostProcessing.hpp" +#include "graphics/core/Shader.hpp" +#include "graphics/core/Shadows.hpp" +#include "graphics/core/Texture.hpp" #include "items/Inventory.hpp" #include "items/ItemDef.hpp" #include "items/ItemStack.hpp" @@ -25,38 +58,6 @@ #include "world/Level.hpp" #include "world/LevelEvents.hpp" #include "world/World.hpp" -#include "graphics/commons/Model.hpp" -#include "graphics/core/Atlas.hpp" -#include "graphics/core/Batch3D.hpp" -#include "graphics/core/DrawContext.hpp" -#include "graphics/core/LineBatch.hpp" -#include "graphics/core/Mesh.hpp" -#include "graphics/core/PostProcessing.hpp" -#include "graphics/core/Framebuffer.hpp" -#include "graphics/core/Shader.hpp" -#include "graphics/core/Texture.hpp" -#include "graphics/core/Font.hpp" -#include "graphics/core/Shadows.hpp" -#include "graphics/core/GBuffer.hpp" -#include "BlockWrapsRenderer.hpp" -#include "ParticlesRenderer.hpp" -#include "PrecipitationRenderer.hpp" -#include "HandsRenderer.hpp" -#include "NamedSkeletons.hpp" -#include "TextsRenderer.hpp" -#include "ChunksRenderer.hpp" -#include "DebugLinesRenderer.hpp" -#include "ModelBatch.hpp" -#include "Skybox.hpp" -#include "Emitter.hpp" -#include "TextNote.hpp" -#include "CloudsRenderer.hpp" - -#include -#include -#include -#include -#include using namespace advanced_pipeline; @@ -105,8 +106,7 @@ WorldRenderer::WorldRenderer( [this](LevelEventType, Chunk* chunk) { chunksRenderer->unload(chunk); } ); skybox = std::make_unique( - settings.graphics.skyboxResolution.get(), - assets.require("skybox_gen") + settings.graphics.skyboxResolution.get(), assets ); const auto& content = level.content; @@ -179,6 +179,11 @@ void WorldRenderer::setupWorldShader( } } +float WorldRenderer::calcFogFactor() const { + const auto& settings = engine.getSettings(); + return 15.0f / static_cast(settings.chunks.loadDistance.get() - 2); +} + void WorldRenderer::renderOpaque( const DrawContext& ctx, const Camera& camera, @@ -187,14 +192,13 @@ void WorldRenderer::renderOpaque( ) { texts->render(ctx, camera, settings, hudVisible, false); - bool culling = engine.getSettings().graphics.frustumCulling.get(); - float fogFactor = - 15.0f / static_cast(settings.chunks.loadDistance.get() - 2); + float fogFactor = calcFogFactor(); auto& entityShader = assets.require("entity"); setupWorldShader(entityShader, camera, settings, fogFactor); skybox->bind(); + bool culling = engine.getSettings().graphics.frustumCulling.get(); if (culling) { frustumCulling->update(camera.getProjView()); } @@ -213,22 +217,24 @@ void WorldRenderer::renderOpaque( auto& shader = assets.require("main"); auto& cloudsShader = assets.require("clouds"); - auto& linesShader = assets.require("lines"); setupWorldShader(shader, camera, settings, fogFactor); chunksRenderer->drawChunks(camera, shader); blockWraps->draw(ctx); - int cloudsQuality = settings.graphics.cloudsQuality.get(); - if (cloudsQuality > 0) { - setupWorldShader(cloudsShader, camera, settings, fogFactor); - cloudsRenderer->draw( - cloudsShader, weather, timer, fogFactor, camera, cloudsQuality - ); + if (level.environment.sky.clouds) { + int cloudsQuality = settings.graphics.cloudsQuality.get(); + if (cloudsQuality > 0) { + setupWorldShader(cloudsShader, camera, settings, fogFactor); + cloudsRenderer->draw( + cloudsShader, weather, timer, fogFactor, camera, cloudsQuality + ); + } } if (hudVisible) { + auto& linesShader = assets.require("lines"); renderLines(camera, linesShader, ctx); } skybox->unbind(); @@ -286,21 +292,19 @@ void WorldRenderer::refreshSettings(Shader** shaders) { int shadowsQuality = graphics.shadowsQuality.get() * gbufferPipeline; shadowMapping->setQuality(shadowsQuality); - + CompileTimeShaderSettings currentSettings { gbufferPipeline, shadowsQuality != 0, - graphics.ssao.get() && gbufferPipeline - }; - if ( - prevCTShaderSettings.advancedRender != currentSettings.advancedRender || + graphics.ssao.get() && gbufferPipeline}; + if (prevCTShaderSettings.advancedRender != currentSettings.advancedRender || prevCTShaderSettings.shadows != currentSettings.shadows || - prevCTShaderSettings.ssao != currentSettings.ssao - ) { + prevCTShaderSettings.ssao != currentSettings.ssao) { std::vector defines; if (currentSettings.shadows) defines.emplace_back("ENABLE_SHADOWS"); if (currentSettings.ssao) defines.emplace_back("ENABLE_SSAO"); - if (currentSettings.advancedRender) defines.emplace_back("ADVANCED_RENDER"); + if (currentSettings.advancedRender) + defines.emplace_back("ADVANCED_RENDER"); for (size_t i = 0; shaders[i]; i++) { shaders[i]->recompile(defines); @@ -332,7 +336,8 @@ void WorldRenderer::renderFrame( auto& entityShader = assets.require("entity"); auto& cloudsShader = assets.require("clouds"); auto& translucentShader = assets.require("translucent"); - auto& deferredShader = assets.require("deferred_lighting").getShader(); + auto& deferredShader = + assets.require("deferred_lighting").getShader(); const auto& settings = engine.getSettings(); @@ -342,19 +347,19 @@ void WorldRenderer::renderFrame( &cloudsShader, &translucentShader, &deferredShader, - nullptr - }; + nullptr}; refreshSettings(affectedShaders); const auto& worldInfo = world->getInfo(); - + float clouds = weather.clouds(); clouds = glm::max(worldInfo.fog, clouds); float mie = 1.0f + glm::max(worldInfo.fog, clouds * 0.5f) * 2.0f; float random = rand() / static_cast(RAND_MAX); skybox->refresh( + level.environment, pctx, worldInfo.daytime, mie, @@ -385,8 +390,7 @@ void WorldRenderer::renderFrame( texts->render(pctx, camera, settings, hudVisible, true); } skybox->bind(); - float fogFactor = - 15.0f / static_cast(settings.chunks.loadDistance.get() - 2); + float fogFactor = calcFogFactor(); if (gbufferPipeline) { deferredShader.use(); setupWorldShader(deferredShader, camera, settings, fogFactor); @@ -403,7 +407,9 @@ void WorldRenderer::renderFrame( } // Background sky plane - skybox->draw(ctx, camera, assets, worldInfo.daytime, clouds); + skybox->draw( + level.environment, ctx, camera, worldInfo.daytime, clouds + ); auto& linesShader = assets.require("lines"); linesShader.use(); @@ -429,14 +435,17 @@ void WorldRenderer::renderFrame( entityShader.use(); setupWorldShader(entityShader, camera, settings, fogFactor); - std::array weatherInstances {&weather.a, &weather.b}; + std::array weatherInstances { + &weather.a, &weather.b}; for (const auto& weather : weatherInstances) { float maxIntensity = weather->fall.maxIntensity; float zero = weather->fall.minOpacity; float one = weather->fall.maxOpacity; float t = (weather->intensity * (one - zero)) * maxIntensity + zero; entityShader.uniform1i("u_alphaClip", weather->fall.opaque); - entityShader.uniform1f("u_opacity", weather->fall.opaque ? t * t : t); + entityShader.uniform1f( + "u_opacity", weather->fall.opaque ? t * t : t + ); if (weather->intensity > 1.e-3f && !weather->fall.texture.empty()) { precipitation->render(camera, *weather); } @@ -446,7 +455,7 @@ void WorldRenderer::renderFrame( glBindFramebuffer(GL_FRAMEBUFFER, 0); } postProcessing.render(pctx, assets, timer, camera); - + if (player.currentCamera == player.fpCamera) { DrawContext ctx = pctx.sub(); ctx.setDepthTest(true); @@ -457,7 +466,7 @@ void WorldRenderer::renderFrame( hudcam.far = 10.0f; hudcam.setFov(0.9f); hudcam.position = {}; - + hands->render(camera); display::clearDepth(); @@ -486,13 +495,12 @@ void WorldRenderer::renderBlockOverlay(const DrawContext& wctx) { if (def.overlayTexture.empty()) { return; } - auto textureRegion = util::get_texture_region( - assets, def.overlayTexture, "blocks:notfound" - ); + auto textureRegion = + util::get_texture_region(assets, def.overlayTexture, "blocks:notfound"); DrawContext ctx = wctx.sub(); ctx.setDepthTest(false); ctx.setCullFace(false); - + auto& shader = assets.require("ui3d"); shader.use(); batch3d->begin(); diff --git a/src/graphics/render/WorldRenderer.hpp b/src/graphics/render/WorldRenderer.hpp index b9a893d16..c3e6b1669 100644 --- a/src/graphics/render/WorldRenderer.hpp +++ b/src/graphics/render/WorldRenderer.hpp @@ -99,6 +99,8 @@ class WorldRenderer { ); void refreshSettings(Shader** shaders); + + float calcFogFactor() const; public: std::unique_ptr particles; std::unique_ptr texts; diff --git a/src/world/Environment.cpp b/src/world/Environment.cpp new file mode 100644 index 000000000..d2ad414bf --- /dev/null +++ b/src/world/Environment.cpp @@ -0,0 +1,25 @@ +#define VC_ENABLE_REFLECTION +#include "Environment.hpp" + +#include "data/dv.hpp" +#include "data/dv_util.hpp" + +dv::value Environment::serialize() const { + return dv::object({ + {"sky", dv::object({ + {"mode", SkyModeMeta.getNameString(sky.mode)}, + {"stars", sky.stars}, + {"clouds", sky.clouds}, + {"sprites", sky.sprites}, + })}, + }); +} + +void Environment::deserialize(const dv::value& src) { + if (auto skyItem = src.at("sky")) { + skyItem->at("mode").get(sky.mode, SkyModeMeta); + skyItem->at("stars").get(sky.stars); + skyItem->at("clouds").get(sky.clouds); + skyItem->at("sprites").get(sky.sprites); + } +} diff --git a/src/world/Environment.hpp b/src/world/Environment.hpp new file mode 100644 index 000000000..5b5f668eb --- /dev/null +++ b/src/world/Environment.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include "interfaces/Serializable.hpp" +#include "util/EnumMetadata.hpp" + +enum class SkyMode { + NONE, SOLID, BOX +}; + +VC_ENUM_METADATA(SkyMode) + {"none", SkyMode::NONE}, + {"solid", SkyMode::SOLID}, + {"box", SkyMode::BOX}, +VC_ENUM_END + +class Environment : public Serializable { +public: + struct { + SkyMode mode = SkyMode::BOX; + bool stars = true; + bool clouds = true; + bool sprites = true; + } sky; + + Environment() = default; + + dv::value serialize() const override; + void deserialize(const dv::value& src) override; +private: +}; diff --git a/src/world/Level.hpp b/src/world/Level.hpp index 0dd3251fa..c8945a23c 100644 --- a/src/world/Level.hpp +++ b/src/world/Level.hpp @@ -6,6 +6,7 @@ #include #include "typedefs.hpp" +#include "Environment.hpp" class Content; class World; @@ -36,6 +37,7 @@ public: std::unique_ptr players; std::unique_ptr pathfinding; std::vector> cameras; // move somewhere? + Environment environment {}; Level( std::unique_ptr world, From 28b77136488d988f7e9eb5311db1ac02f8481c2b Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 12 May 2026 18:41:14 +0300 Subject: [PATCH 002/217] move some code there and here --- src/graphics/core/DrawContext.cpp | 22 +++++++++++++ src/graphics/core/DrawContext.hpp | 3 ++ src/graphics/core/Framebuffer.cpp | 4 +-- src/graphics/core/Framebuffer.hpp | 4 +-- src/graphics/core/GBuffer.cpp | 4 +-- src/graphics/core/GBuffer.hpp | 4 +-- src/graphics/core/Texture.hpp | 15 +++++---- src/graphics/core/commons.hpp | 6 ++-- src/graphics/render/HandsRenderer.cpp | 3 ++ src/graphics/render/Skybox.cpp | 46 +++++++++------------------ src/graphics/render/Skybox.hpp | 6 ++-- src/graphics/render/WorldRenderer.cpp | 43 +++++++++++-------------- 12 files changed, 84 insertions(+), 76 deletions(-) diff --git a/src/graphics/core/DrawContext.cpp b/src/graphics/core/DrawContext.cpp index fc37fd46d..86e2131b1 100644 --- a/src/graphics/core/DrawContext.cpp +++ b/src/graphics/core/DrawContext.cpp @@ -74,6 +74,18 @@ DrawContext::~DrawContext() { if (lineWidth != parent->lineWidth) { glLineWidth(parent->lineWidth); } + + for (int i = 0; i < textures.size(); i++) { + auto texture = parent->textures[i]; + if (textures[i] != texture) { + glActiveTexture(GL_TEXTURE0 + i); + if (texture == nullptr) { + glBindTexture(GL_TEXTURE_2D, 0); + } else { + texture->bind(); + } + } + } } const glm::uvec2& DrawContext::getViewport() const { @@ -154,3 +166,13 @@ void DrawContext::setLineWidth(float width) { lineWidth = width; glLineWidth(width); } + +void DrawContext::useTexture(int target, const Bindable* texture) { + glActiveTexture(GL_TEXTURE0 + target); + if (texture == nullptr) { + glBindTexture(GL_TEXTURE_2D, 0); + } else { + texture->bind(); + } + textures.at(target) = texture; +} diff --git a/src/graphics/core/DrawContext.hpp b/src/graphics/core/DrawContext.hpp index b14bf71bc..7e568e337 100644 --- a/src/graphics/core/DrawContext.hpp +++ b/src/graphics/core/DrawContext.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -23,6 +24,7 @@ class DrawContext { BlendMode blendMode = BlendMode::normal; int scissorsCount = 0; float lineWidth = 1.0f; + std::array textures {}; public: DrawContext( const DrawContext* parent, @@ -44,4 +46,5 @@ public: void setBlendMode(BlendMode mode); void setScissors(const glm::vec4& area); void setLineWidth(float width); + void useTexture(int target, const Bindable* texture); }; diff --git a/src/graphics/core/Framebuffer.cpp b/src/graphics/core/Framebuffer.cpp index dde31f6be..1dc677096 100644 --- a/src/graphics/core/Framebuffer.cpp +++ b/src/graphics/core/Framebuffer.cpp @@ -84,11 +84,11 @@ void Framebuffer::setTexture(std::unique_ptr texture) { this->texture = std::move(texture); } -void Framebuffer::bind() { +void Framebuffer::bind() const { glBindFramebuffer(GL_FRAMEBUFFER, fbo); } -void Framebuffer::unbind() { +void Framebuffer::unbind() const { glBindFramebuffer(GL_FRAMEBUFFER, 0); } diff --git a/src/graphics/core/Framebuffer.hpp b/src/graphics/core/Framebuffer.hpp index f64d33f37..5f9dc3491 100644 --- a/src/graphics/core/Framebuffer.hpp +++ b/src/graphics/core/Framebuffer.hpp @@ -21,10 +21,10 @@ public: void setTexture(std::unique_ptr texture); /// @brief Use framebuffer - void bind() override; + void bind() const override; /// @brief Stop using framebuffer - void unbind() override; + void unbind() const override; /// @brief Update framebuffer texture size /// @param width new width diff --git a/src/graphics/core/GBuffer.cpp b/src/graphics/core/GBuffer.cpp index aba3103fc..30b0c817a 100644 --- a/src/graphics/core/GBuffer.cpp +++ b/src/graphics/core/GBuffer.cpp @@ -189,7 +189,7 @@ GBuffer::~GBuffer() { glDeleteFramebuffers(1, &ssaoFbo); } -void GBuffer::bind() { +void GBuffer::bind() const { glBindFramebuffer(GL_FRAMEBUFFER, fbo); glClear(GL_COLOR_BUFFER_BIT); } @@ -198,7 +198,7 @@ void GBuffer::bindSSAO() const { glBindFramebuffer(GL_FRAMEBUFFER, ssaoFbo); } -void GBuffer::unbind() { +void GBuffer::unbind() const { glBindFramebuffer(GL_FRAMEBUFFER, 0); } diff --git a/src/graphics/core/GBuffer.hpp b/src/graphics/core/GBuffer.hpp index ae433c873..a549b720b 100644 --- a/src/graphics/core/GBuffer.hpp +++ b/src/graphics/core/GBuffer.hpp @@ -9,9 +9,9 @@ public: GBuffer(uint width, uint height); ~GBuffer() override; - void bind() override; + void bind() const override; void bindSSAO() const; - void unbind() override; + void unbind() const override; void bindBuffers() const; void bindSSAOBuffer() const; diff --git a/src/graphics/core/Texture.hpp b/src/graphics/core/Texture.hpp index 50734a577..30959ed60 100644 --- a/src/graphics/core/Texture.hpp +++ b/src/graphics/core/Texture.hpp @@ -1,12 +1,13 @@ #pragma once -#include "typedefs.hpp" -#include "maths/UVRegion.hpp" -#include "ImageData.hpp" - #include -class Texture { +#include "ImageData.hpp" +#include "commons.hpp" +#include "maths/UVRegion.hpp" +#include "typedefs.hpp" + +class Texture : public Bindable { protected: uint id; uint width; @@ -17,8 +18,8 @@ public: Texture(const ubyte* data, uint width, uint height, ImageFormat format); virtual ~Texture(); - virtual void bind() const; - virtual void unbind() const; + virtual void bind() const override; + virtual void unbind() const override; virtual void reload(const ubyte* data, uint w, uint h); void reloadPartial(const ImageData& image, uint x, uint y, uint w, uint h); diff --git a/src/graphics/core/commons.hpp b/src/graphics/core/commons.hpp index 8785cec10..0ca5a6470 100644 --- a/src/graphics/core/commons.hpp +++ b/src/graphics/core/commons.hpp @@ -59,6 +59,8 @@ namespace advanced_pipeline { inline constexpr int TARGET_SSAO = 5; inline constexpr int TARGET_SHADOWS0 = 6; inline constexpr int TARGET_SHADOWS1 = 7; + + inline constexpr int TARGETS_COUNT = 8; } VC_ENUM_METADATA(CursorShape) @@ -85,6 +87,6 @@ class Bindable { public: virtual ~Bindable() = default; - virtual void bind() = 0; - virtual void unbind() = 0; + virtual void bind() const = 0; + virtual void unbind() const = 0; }; diff --git a/src/graphics/render/HandsRenderer.cpp b/src/graphics/render/HandsRenderer.cpp index 8e2f757b2..17c38d76d 100644 --- a/src/graphics/render/HandsRenderer.cpp +++ b/src/graphics/render/HandsRenderer.cpp @@ -34,4 +34,7 @@ void HandsRenderer::render(const Camera& camera) { glm::vec3(), glm::vec3(1.0f) ); + + modelBatch.render(); + modelBatch.setLightsOffset(glm::vec3()); } diff --git a/src/graphics/render/Skybox.cpp b/src/graphics/render/Skybox.cpp index 79e5552af..ef34384fc 100644 --- a/src/graphics/render/Skybox.cpp +++ b/src/graphics/render/Skybox.cpp @@ -80,9 +80,11 @@ void Skybox::drawBackground(const Camera& camera, int width, int height) { ); backShader->uniform1f("u_ar", float(width) / float(height)); backShader->uniform1i("u_skybox", 1); - bind(); + + auto texture = fbo->getTexture(); + texture->bind(); mesh->draw(); - unbind(); + texture->unbind(); } void Skybox::drawStars(float angle, float opacity) { @@ -150,9 +152,6 @@ void Skybox::draw( float fog ) { const glm::uvec2& viewport = pctx.getViewport(); - - glActiveTexture(GL_TEXTURE0); - drawBackground(camera, viewport.x, viewport.y); float angle = daytime * glm::pi() * 2.0f; @@ -216,13 +215,12 @@ void Skybox::refresh( ctx.setDepthTest(false); ctx.setFramebuffer(fbo.get()); ctx.setViewport({fbo->getWidth(), fbo->getHeight()}); + ctx.useTexture(advanced_pipeline::TARGET_SKYBOX, fbo->getTexture()); + ctx.useTexture(advanced_pipeline::TARGET_COLOR, nullptr); auto cubemap = dynamic_cast(fbo->getTexture()); assert(cubemap != nullptr); - glActiveTexture(GL_TEXTURE0 + TARGET_SKYBOX); - cubemap->bind(); - for (int face = 0; face < 6; face++) { glFramebufferTexture2D( GL_FRAMEBUFFER, @@ -234,8 +232,6 @@ void Skybox::refresh( glClearColor(tint.r, tint.g, tint.b, 1); glClear(GL_COLOR_BUFFER_BIT); } - - cubemap->unbind(); return; } @@ -245,14 +241,12 @@ void Skybox::refresh( ctx.setDepthTest(false); ctx.setFramebuffer(fbo.get()); ctx.setViewport({fbo->getWidth(), fbo->getHeight()}); + ctx.useTexture(advanced_pipeline::TARGET_SKYBOX, fbo->getTexture()); + ctx.useTexture(advanced_pipeline::TARGET_COLOR, nullptr); auto cubemap = dynamic_cast(fbo->getTexture()); assert(cubemap != nullptr); - glActiveTexture(GL_TEXTURE0 + TARGET_SKYBOX); - cubemap->bind(); - shader.use(); - float t = dayTime * glm::two_pi(); lightDir = glm::normalize(glm::vec3(sin(t), -cos(t), 0.0f)); @@ -266,6 +260,7 @@ void Skybox::refresh( rotation = glm::rotate(rotation, z, glm::vec3(0, 0, 1)); lightDir = glm::vec3(rotation * glm::vec4(0, 0, -1, 1)); + shader.use(); shader.uniform1i("u_quality", quality); shader.uniform1f("u_mie", mie); shader.uniform3f("u_tint", tint); @@ -278,21 +273,18 @@ void Skybox::refresh( glm::abs(prevHighlight.r - hightlight.r) >= 0.01) { for (uint face = 0; face < 6; face++) { - refreshFace(face, cubemap); + refreshFace(face, *cubemap); } } else { uint face = frameid % 6; - refreshFace(face, cubemap); + refreshFace(face, *cubemap); } prevMie = mie; prevT = t; prevHighlight = hightlight; - - cubemap->unbind(); - glActiveTexture(GL_TEXTURE0); } -void Skybox::refreshFace(uint face, Cubemap* cubemap) { +void Skybox::refreshFace(uint face, Cubemap& cubemap) { const glm::vec3 xaxs[] = { {0.0f, 0.0f, -1.0f}, {0.0f, 0.0f, 1.0f}, @@ -325,7 +317,7 @@ void Skybox::refreshFace(uint face, Cubemap* cubemap) { GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, - cubemap->getId(), + cubemap.getId(), 0 ); shader.uniform3f("u_xaxis", xaxs[face]); @@ -334,14 +326,6 @@ void Skybox::refreshFace(uint face, Cubemap* cubemap) { mesh->draw(); } -void Skybox::bind() const { - glActiveTexture(GL_TEXTURE0 + TARGET_SKYBOX); - fbo->getTexture()->bind(); - glActiveTexture(GL_TEXTURE0); -} - -void Skybox::unbind() const { - glActiveTexture(GL_TEXTURE0 + TARGET_SKYBOX); - fbo->getTexture()->unbind(); - glActiveTexture(GL_TEXTURE0); +const Cubemap* Skybox::getCubemap() const { + return dynamic_cast(fbo->getTexture()); } diff --git a/src/graphics/render/Skybox.hpp b/src/graphics/render/Skybox.hpp index 270dc066d..6d6074e6e 100644 --- a/src/graphics/render/Skybox.hpp +++ b/src/graphics/render/Skybox.hpp @@ -59,7 +59,7 @@ class Skybox { void drawStars(float angle, float opacity); void drawBackground(const Camera& camera, int width, int height); void drawSkySprites(float daytime, float angle, float opacity); - void refreshFace(uint face, Cubemap* cubemap); + void refreshFace(uint face, Cubemap& cubemap); public: Skybox(uint size, const Assets& assets); ~Skybox(); @@ -83,8 +83,8 @@ public: const glm::vec3& hightlight, uint quality ); - void bind() const; - void unbind() const; + + const Cubemap* getCubemap() const; const glm::vec3& getLightDir() const { return lightDir; diff --git a/src/graphics/render/WorldRenderer.cpp b/src/graphics/render/WorldRenderer.cpp index e5d838f7d..57cc90f24 100644 --- a/src/graphics/render/WorldRenderer.cpp +++ b/src/graphics/render/WorldRenderer.cpp @@ -1,12 +1,5 @@ #include "WorldRenderer.hpp" -#include - -#include -#include -#include -#include - #include "BlockWrapsRenderer.hpp" #include "ChunksRenderer.hpp" #include "CloudsRenderer.hpp" @@ -39,6 +32,7 @@ #include "graphics/core/PostProcessing.hpp" #include "graphics/core/Shader.hpp" #include "graphics/core/Shadows.hpp" +#include "graphics/core/Cubemap.hpp" #include "graphics/core/Texture.hpp" #include "items/Inventory.hpp" #include "items/ItemDef.hpp" @@ -59,7 +53,11 @@ #include "world/LevelEvents.hpp" #include "world/World.hpp" -using namespace advanced_pipeline; +#include +#include +#include +#include +#include inline constexpr size_t BATCH3D_CAPACITY = 4096; inline constexpr size_t MODEL_BATCH_CAPACITY = 20'000; @@ -160,7 +158,7 @@ void WorldRenderer::setupWorldShader( shader.uniform1i("u_debugNormals", false); shader.uniform1f("u_dayTime", level.getWorld()->getInfo().daytime); shader.uniform2f("u_lightDir", skybox->getLightDir()); - shader.uniform1i("u_skybox", TARGET_SKYBOX); + shader.uniform1i("u_skybox", advanced_pipeline::TARGET_SKYBOX); auto indices = level.content.getIndices(); // Light emission when an emissive item is chosen @@ -196,7 +194,6 @@ void WorldRenderer::renderOpaque( auto& entityShader = assets.require("entity"); setupWorldShader(entityShader, camera, settings, fogFactor); - skybox->bind(); bool culling = engine.getSettings().graphics.frustumCulling.get(); if (culling) { @@ -237,7 +234,6 @@ void WorldRenderer::renderOpaque( auto& linesShader = assets.require("lines"); renderLines(camera, linesShader, ctx); } - skybox->unbind(); } void WorldRenderer::renderBlockSelection() { @@ -378,18 +374,18 @@ void WorldRenderer::renderFrame( { DrawContext wctx = pctx.sub(); postProcessing.use(wctx, gbufferPipeline); - display::clearDepth(); /* Main opaque pass (GBuffer pass) */ { DrawContext ctx = wctx.sub(); ctx.setDepthTest(true); ctx.setCullFace(true); + ctx.useTexture(advanced_pipeline::TARGET_SKYBOX, skybox->getCubemap()); + ctx.useTexture(advanced_pipeline::TARGET_COLOR, nullptr); renderOpaque(ctx, camera, settings, hudVisible); } texts->render(pctx, camera, settings, hudVisible, true); } - skybox->bind(); float fogFactor = calcFogFactor(); if (gbufferPipeline) { deferredShader.use(); @@ -399,6 +395,7 @@ void WorldRenderer::renderFrame( { DrawContext ctx = pctx.sub(); ctx.setDepthTest(true); + ctx.useTexture(advanced_pipeline::TARGET_COLOR, nullptr); if (gbufferPipeline) { postProcessing.bindDepthBuffer(); @@ -421,10 +418,12 @@ void WorldRenderer::renderFrame( linesShader.uniformMatrix("u_projview", projView); lineBatch->flush(); - skybox->bind(); + DrawContext wctx = ctx.sub(); + wctx.useTexture(advanced_pipeline::TARGET_SKYBOX, skybox->getCubemap()); + wctx.useTexture(advanced_pipeline::TARGET_COLOR, nullptr); // Translucent blocks { - auto sctx = ctx.sub(); + auto sctx = wctx.sub(); sctx.setCullFace(true); translucentShader.use(); setupWorldShader(translucentShader, camera, settings, fogFactor); @@ -450,8 +449,6 @@ void WorldRenderer::renderFrame( precipitation->render(camera, *weather); } } - skybox->unbind(); - glBindFramebuffer(GL_FRAMEBUFFER, 0); } postProcessing.render(pctx, assets, timer, camera); @@ -460,6 +457,9 @@ void WorldRenderer::renderFrame( DrawContext ctx = pctx.sub(); ctx.setDepthTest(true); ctx.setCullFace(true); + ctx.useTexture(advanced_pipeline::TARGET_SKYBOX, skybox->getCubemap()); + ctx.useTexture(advanced_pipeline::TARGET_COLOR, nullptr); + display::clearDepth(); // prepare modified HUD camera Camera hudcam = camera; @@ -467,15 +467,8 @@ void WorldRenderer::renderFrame( hudcam.setFov(0.9f); hudcam.position = {}; - hands->render(camera); - - display::clearDepth(); setupWorldShader(entityShader, hudcam, engine.getSettings(), 0.0f); - - skybox->bind(); - modelBatch->render(); - modelBatch->setLightsOffset(glm::vec3()); - skybox->unbind(); + hands->render(camera); } renderBlockOverlay(pctx); From 4aaa5b2d7e4d4896ad740d1c36c732eb24f7a527 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 12 May 2026 20:45:48 +0300 Subject: [PATCH 003/217] move sky sprites to environment --- src/graphics/render/Skybox.cpp | 35 +++++---------------------------- src/graphics/render/Skybox.hpp | 17 ++++++---------- src/world/Environment.cpp | 26 ++++++++++++++++++++++-- src/world/Environment.hpp | 36 +++++++++++++++++++++++++++++++++- 4 files changed, 70 insertions(+), 44 deletions(-) diff --git a/src/graphics/render/Skybox.cpp b/src/graphics/render/Skybox.cpp index ef34384fc..9cdf45785 100644 --- a/src/graphics/render/Skybox.cpp +++ b/src/graphics/render/Skybox.cpp @@ -23,7 +23,6 @@ using namespace advanced_pipeline; const int STARS_COUNT = 3000; - const int STARS_SEED = 632; Skybox::Skybox(uint size, const Assets& assets) @@ -41,31 +40,6 @@ Skybox::Skybox(uint size, const Assets& assets) {{1.0f, -1.0f}}}; mesh = std::make_unique>(vertices, 6); - - sprites.push_back(SkySprite { - "misc/moon", - glm::pi() * 0.5f, - 4.0f, - false, - glm::pi() * 0.25f, - }); - - sprites.push_back(SkySprite { - "misc/moon_flare", - glm::pi() * 0.5f, - 0.5f, - false, - glm::pi() * 0.25f, - }); - - sprites.push_back(SkySprite { - "misc/sun", - glm::pi() * 1.5f, - 4.0f, - true, - glm::pi() * 0.25f, - }); - setMode(mode); } @@ -117,7 +91,8 @@ void Skybox::drawStars(float angle, float opacity) { void Skybox::drawSkySprites( float daytime, float angle, - float opacity + float opacity, + const std::vector& sprites ) { float depthScale = 2e3; for (auto& sprite : sprites) { @@ -160,7 +135,7 @@ void Skybox::draw( DrawContext ctx = pctx.sub(); ctx.setBlendMode(BlendMode::addition); - if (!environment.sky.sprites && !environment.sky.stars) { + if (environment.sky.sprites.empty() && !environment.sky.stars) { return; } @@ -170,8 +145,8 @@ void Skybox::draw( shader->uniformMatrix("u_apply", glm::mat4(1.0f)); batch3d->begin(); - if (environment.sky.sprites) { - drawSkySprites(daytime, angle, opacity); + if (!environment.sky.sprites.empty()) { + drawSkySprites(daytime, angle, opacity, environment.sky.sprites); } if (environment.sky.stars) { drawStars(angle, opacity); diff --git a/src/graphics/render/Skybox.hpp b/src/graphics/render/Skybox.hpp index 6d6074e6e..0420bc55e 100644 --- a/src/graphics/render/Skybox.hpp +++ b/src/graphics/render/Skybox.hpp @@ -10,7 +10,6 @@ #include "typedefs.hpp" #include "world/Environment.hpp" - template class Mesh; class Shader; @@ -28,14 +27,6 @@ struct SkyboxVertex { {VertexAttribute::Type::FLOAT, false, 2}, {{}, 0}}; }; -struct SkySprite { - std::string texture; - float phase; - float distance; - bool emissive; - float altitude; -}; - class Skybox { SkyMode mode = SkyMode::SOLID; uint size; @@ -47,7 +38,6 @@ class Skybox { std::unique_ptr> mesh; std::unique_ptr batch3d; - std::vector sprites; int frameid = 0; float prevMie = -1.0f; @@ -58,7 +48,12 @@ class Skybox { void drawStars(float angle, float opacity); void drawBackground(const Camera& camera, int width, int height); - void drawSkySprites(float daytime, float angle, float opacity); + void drawSkySprites( + float daytime, + float angle, + float opacity, + const std::vector& sprites + ); void refreshFace(uint face, Cubemap& cubemap); public: Skybox(uint size, const Assets& assets); diff --git a/src/world/Environment.cpp b/src/world/Environment.cpp index d2ad414bf..932880200 100644 --- a/src/world/Environment.cpp +++ b/src/world/Environment.cpp @@ -5,12 +5,22 @@ #include "data/dv_util.hpp" dv::value Environment::serialize() const { + auto skySpritesList = dv::list(); + for (const auto& sprite : sky.sprites) { + skySpritesList.add(dv::object({ + {"texture", sprite.texture}, + {"phase", sprite.phase}, + {"distance", sprite.distance}, + {"emissive", sprite.emissive}, + {"altitude", sprite.altitude}, + })); + } return dv::object({ {"sky", dv::object({ {"mode", SkyModeMeta.getNameString(sky.mode)}, {"stars", sky.stars}, {"clouds", sky.clouds}, - {"sprites", sky.sprites}, + {"sprites", std::move(skySpritesList)}, })}, }); } @@ -20,6 +30,18 @@ void Environment::deserialize(const dv::value& src) { skyItem->at("mode").get(sky.mode, SkyModeMeta); skyItem->at("stars").get(sky.stars); skyItem->at("clouds").get(sky.clouds); - skyItem->at("sprites").get(sky.sprites); + + if (auto skySpritesList = skyItem->at("sprites")) { + for (int i = 0; i < skySpritesList->size(); i++) { + SkySprite sprite; + const auto& item = (*skySpritesList.ptr)[i]; + item.at("texture").get(sprite.texture); + item.at("phase").get(sprite.phase); + item.at("distance").get(sprite.distance); + item.at("emissive").get(sprite.emissive); + item.at("altitude").get(sprite.altitude); + sky.sprites.push_back(std::move(sprite)); + } + } } } diff --git a/src/world/Environment.hpp b/src/world/Environment.hpp index 5b5f668eb..feb244178 100644 --- a/src/world/Environment.hpp +++ b/src/world/Environment.hpp @@ -3,6 +3,9 @@ #include "interfaces/Serializable.hpp" #include "util/EnumMetadata.hpp" +#include +#include + enum class SkyMode { NONE, SOLID, BOX }; @@ -13,13 +16,44 @@ VC_ENUM_METADATA(SkyMode) {"box", SkyMode::BOX}, VC_ENUM_END +struct SkySprite { + std::string texture; + float phase; + float distance; + bool emissive; + float altitude; +}; + class Environment : public Serializable { public: struct { SkyMode mode = SkyMode::BOX; bool stars = true; bool clouds = true; - bool sprites = true; + // TODO: move defaults to config file + std::vector sprites { + SkySprite { + "misc/moon", + glm::pi() * 0.5f, + 4.0f, + false, + glm::pi() * 0.25f, + }, + SkySprite { + "misc/moon_flare", + glm::pi() * 0.5f, + 0.5f, + false, + glm::pi() * 0.25f, + }, + SkySprite { + "misc/sun", + glm::pi() * 1.5f, + 4.0f, + true, + glm::pi() * 0.25f, + }, + }; } sky; Environment() = default; From 3476bddc6b2a10b05a6a78f9db88c9e35ae5df5e Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sat, 16 May 2026 22:24:35 +0300 Subject: [PATCH 004/217] feat: environment is primary --- src/logic/ChunksController.cpp | 2 +- src/logic/EngineController.cpp | 20 +++++++++-- src/logic/LevelController.cpp | 2 +- src/logic/scripting/lua/libs/libworld.cpp | 2 +- src/objects/Player.cpp | 2 +- src/world/Environment.cpp | 2 ++ src/world/Environment.hpp | 2 ++ src/world/Level.cpp | 1 + src/world/World.cpp | 44 ++++++++++++++--------- src/world/World.hpp | 12 ++++--- 10 files changed, 60 insertions(+), 29 deletions(-) diff --git a/src/logic/ChunksController.cpp b/src/logic/ChunksController.cpp index 7a460c066..1dc791b19 100644 --- a/src/logic/ChunksController.cpp +++ b/src/logic/ChunksController.cpp @@ -25,7 +25,7 @@ const uint MIN_SURROUNDING = 9; ChunksController::ChunksController(Level& level) : level(level), generator(std::make_unique( - level.content.generators.require(level.getWorld()->getGenerator()), + level.content.generators.require(level.environment.generator), level.content, level.getWorld()->getSeed() )) {} diff --git a/src/logic/EngineController.cpp b/src/logic/EngineController.cpp index 9231ffe67..7637b250e 100644 --- a/src/logic/EngineController.cpp +++ b/src/logic/EngineController.cpp @@ -179,7 +179,7 @@ void EngineController::onMissingContent( if (engine.isHeadless()) { throw std::runtime_error( "missing content: " + - json::stringify(create_missing_content_report(report), true, " ") + util::quote(json::stringify(create_missing_content_report(report), true, " ")) ); } else { engine.setScreen(std::make_shared(engine)); @@ -268,7 +268,7 @@ inline uint64_t str2seed(const std::string& seedstr) { void EngineController::createWorld( const std::string& name, const std::string& seedstr, - const std::string& generatorID + const std::string& environment ) { uint64_t seed = str2seed(seedstr); @@ -279,17 +279,31 @@ void EngineController::createWorld( paths.setCurrentWorldFolder(folder); engine.getContentControl().loadContent(); }); + const auto& generators = engine.getContentControl().get()->generators; + + // generator-based environments must die in 1.0 + bool genBasedEnv = false; + if (auto genDef = generators.find(environment)) { + genBasedEnv = true; + } auto& contentControl = engine.getContentControl(); auto level = World::create( name, - generatorID, + genBasedEnv ? "" : environment, + genBasedEnv ? environment : "", folder, seed, engine.getSettings(), *contentControl.get(), contentControl.getContentPacks() ); + + // generator-based environment initialization + if (genBasedEnv) { + level->environment.generator = environment; + } + if (!engine.isHeadless()) { level->players->create(localPlayer); } diff --git a/src/logic/LevelController.cpp b/src/logic/LevelController.cpp index 870fed923..6f992ba4c 100644 --- a/src/logic/LevelController.cpp +++ b/src/logic/LevelController.cpp @@ -149,7 +149,7 @@ void LevelController::saveWorld() { world->wfile->createDirectories(); scripting::on_world_save(); level->onSave(); - level->getWorld()->write(level.get()); + level->getWorld()->write(*level); } void LevelController::onWorldQuit() { diff --git a/src/logic/scripting/lua/libs/libworld.cpp b/src/logic/scripting/lua/libs/libworld.cpp index c2d6be43c..9c50b6c0b 100644 --- a/src/logic/scripting/lua/libs/libworld.cpp +++ b/src/logic/scripting/lua/libs/libworld.cpp @@ -125,7 +125,7 @@ static int l_is_night(lua::State* L) { } static int l_get_generator(lua::State* L) { - return lua::pushstring(L, require_world_info().generator); + return lua::pushstring(L, require_level().environment.generator); } static int l_get_chunk_data(lua::State* L) { diff --git a/src/objects/Player.cpp b/src/objects/Player.cpp index fec65d7de..7d921a4fa 100644 --- a/src/objects/Player.cpp +++ b/src/objects/Player.cpp @@ -127,7 +127,7 @@ void Player::teleport(glm::vec3 position) { void Player::attemptToChooseSpawnpoint() { // looks bad to be here tbh const auto& generatorDef = - level.content.generators.require(level.getWorld()->getGenerator()); + level.content.generators.require(level.environment.generator); int minHeight = generatorDef.playerMinSpawnHeight; int maxHeight = generatorDef.playerMaxSpawnHeight; diff --git a/src/world/Environment.cpp b/src/world/Environment.cpp index 932880200..29c3ef476 100644 --- a/src/world/Environment.cpp +++ b/src/world/Environment.cpp @@ -22,6 +22,7 @@ dv::value Environment::serialize() const { {"clouds", sky.clouds}, {"sprites", std::move(skySpritesList)}, })}, + {"generator", generator} }); } @@ -44,4 +45,5 @@ void Environment::deserialize(const dv::value& src) { } } } + src.at("generator").get(generator); } diff --git a/src/world/Environment.hpp b/src/world/Environment.hpp index feb244178..c2efa828a 100644 --- a/src/world/Environment.hpp +++ b/src/world/Environment.hpp @@ -56,6 +56,8 @@ public: }; } sky; + std::string generator; + Environment() = default; dv::value serialize() const override; diff --git a/src/world/Level.cpp b/src/world/Level.cpp index e6d9000e1..9e40e90cd 100644 --- a/src/world/Level.cpp +++ b/src/world/Level.cpp @@ -31,6 +31,7 @@ Level::Level( entities(std::make_unique(*this)), players(std::make_unique(*this)), pathfinding(std::make_unique(*this)) { + const auto& worldInfo = world->getInfo(); auto& cameraIndices = content.getIndices(ResourceType::CAMERA); for (size_t i = 0; i < cameraIndices.size(); i++) { diff --git a/src/world/World.cpp b/src/world/World.cpp index 6508a4dab..2acc7edaa 100644 --- a/src/world/World.cpp +++ b/src/world/World.cpp @@ -65,12 +65,12 @@ void World::writeResources(const Content& content) { io::write_json(wfile->getResourcesFile(), root); } -void World::write(Level* level) { - level->chunks->saveAll(); - info.nextEntityId = level->entities->peekNextID(); +void World::write(Level& level) { + level.chunks->saveAll(); + info.nextEntityId = level.entities->peekNextID(); wfile->write(this, &content); - auto playerFile = level->players->serialize(); + auto playerFile = level.players->serialize(); io::write_json(wfile->getPlayerFile(), playerFile); writeResources(content); @@ -78,7 +78,8 @@ void World::write(Level* level) { std::unique_ptr World::create( const std::string& name, - const std::string& generator, + const std::string& environment, + const std::string& generatorOverride, const io::path& directory, uint64_t seed, EngineSettings& settings, @@ -87,7 +88,8 @@ std::unique_ptr World::create( ) { WorldInfo info {}; info.name = name; - info.generator = generator; + info.environment = environment; + info.explicitGenerator = generatorOverride; info.seed = seed; auto world = std::make_unique( info, @@ -101,7 +103,7 @@ std::unique_ptr World::create( logger.info() << "created world '" << name << "' (" << directory.string() << ")"; } - logger.info() << "world seed: " << seed << " generator: " << generator; + logger.info() << "world seed: " << seed << " environment: " << environment; return std::make_unique(std::move(world), content, settings); } @@ -122,16 +124,19 @@ std::unique_ptr World::load( << worldFilesPtr->getFolder().string() << ")"; logger.info() << "world version: " << info->major << "." << info->minor << " seed: " << info->seed - << " generator: " << info->generator; + << " environment: " << info->environment; - auto world = std::make_unique( + auto worldPtr = std::make_unique( info.value(), std::move(worldFilesPtr), content, packs ); + auto world = worldPtr.get(); auto& wfile = world->wfile; wfile->readResourcesData(content); - auto level = std::make_unique(std::move(world), content, settings); - + auto level = std::make_unique(std::move(worldPtr), content, settings); + if (world->getEnvironment().empty()) { + level->environment.generator = world->getInfo().explicitGenerator; + } io::path file = wfile->getPlayerFile(); if (!io::is_regular_file(file)) { logger.warning() << "player.json does not exists"; @@ -163,8 +168,8 @@ void World::setName(const std::string& name) { this->info.name = name; } -void World::setGenerator(const std::string& generator) { - this->info.generator = generator; +void World::setEnvironment(const std::string& environment) { + this->info.environment = environment; } bool World::hasPack(const std::string& id) const { @@ -186,8 +191,8 @@ uint64_t World::getSeed() const { return info.seed; } -std::string World::getGenerator() const { - return info.generator; +std::string World::getEnvironment() const { + return info.environment; } const std::vector& World::getPacks() const { @@ -196,7 +201,8 @@ const std::vector& World::getPacks() const { void WorldInfo::deserialize(const dv::value& root) { name = root["name"].asString(); - generator = root["generator"].asString(generator); + root.at("environment").get(environment); + root.at("generator").get(explicitGenerator); seed = root["seed"].asInteger(seed); if (root.has("version")) { @@ -226,7 +232,11 @@ dv::value WorldInfo::serialize() const { versionobj["minor"] = ENGINE_VERSION_MINOR; root["name"] = name; - root["generator"] = generator; + if (environment.empty()) { + root["generator"] = explicitGenerator; + } else { + root["environment"] = environment; + } root["seed"] = seed; auto& timeobj = root.object("time"); diff --git a/src/world/World.hpp b/src/world/World.hpp index 725b91823..14649fb1b 100644 --- a/src/world/World.hpp +++ b/src/world/World.hpp @@ -23,7 +23,8 @@ public: struct WorldInfo : public Serializable { std::string name; - std::string generator; + std::string environment; + std::string explicitGenerator; uint64_t seed; int64_t nextInventoryId = 1; int64_t nextPlayerId = 0; @@ -76,7 +77,7 @@ public: void updateTimers(float delta); /// @brief Write all unsaved level data to the world directory - void write(Level* level); + void write(Level& level); /// @brief Check world indices and generate ContentReport if convert required /// @param directory world directory @@ -98,7 +99,8 @@ public: /// @return Level instance containing World instance static std::unique_ptr create( const std::string& name, - const std::string& generator, + const std::string& environment, + const std::string& generatorOverride, const io::path& directory, uint64_t seed, EngineSettings& settings, @@ -123,7 +125,7 @@ public: void setName(const std::string& name); void setSeed(uint64_t seed); - void setGenerator(const std::string& generator); + void setEnvironment(const std::string& environment); /// @brief Check if world has content-pack installed /// @param id content-pack id @@ -137,7 +139,7 @@ public: uint64_t getSeed() const; /// @brief Get world generator id - std::string getGenerator() const; + std::string getEnvironment() const; bool isNameless() const { return info.name.empty(); From b2ed4b472f9b93f796381300859f58dcd12aac4f Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 1 Jun 2026 01:14:30 +0300 Subject: [PATCH 005/217] feat: client-side '--script' and '--test' basic support --- src/devtools/AppScriptsControl.cpp | 59 ++++++++++++++++++++++++++++++ src/devtools/AppScriptsControl.hpp | 28 ++++++++++++++ src/devtools/Project.cpp | 21 +---------- src/devtools/Project.hpp | 14 +------ src/engine/Engine.cpp | 20 +++++----- src/engine/Engine.hpp | 2 + src/engine/Mainloop.cpp | 11 +++++- src/engine/ServerMainloop.cpp | 14 +------ 8 files changed, 112 insertions(+), 57 deletions(-) create mode 100644 src/devtools/AppScriptsControl.cpp create mode 100644 src/devtools/AppScriptsControl.hpp diff --git a/src/devtools/AppScriptsControl.cpp b/src/devtools/AppScriptsControl.cpp new file mode 100644 index 000000000..16c3475d5 --- /dev/null +++ b/src/devtools/AppScriptsControl.cpp @@ -0,0 +1,59 @@ +#include "AppScriptsControl.hpp" + +#include "debug/Logger.hpp" +#include "engine/CoreParameters.hpp" +#include "io/io.hpp" +#include "Project.hpp" +#include "logic/scripting/scripting.hpp" + +static debug::Logger logger("app-scripts"); + +AppScriptsControl::AppScriptsControl( + const CoreParameters& params, const Project& project +) + : project(project) { + + io::path scriptFile = + params.scriptFile.empty() + ? "project:start.lua" + : std::string("script:") + params.scriptFile.filename().u8string(); + if (io::exists(scriptFile)) { + logger.info() << "starting script: " << params.scriptFile.u8string(); + scriptCoroutine = scripting::start_app_script(scriptFile); + } else { + logger.warning() << "script does not exists: " << params.scriptFile.u8string(); + } + + if (!params.headless) { + loadProjectClientScript(); + } +} + +void AppScriptsControl::loadProjectClientScript() { + io::path scriptFile = "project:project_client.lua"; + if (io::exists(scriptFile)) { + logger.info() << "starting project client script: " << scriptFile.string(); + clientScript = scripting::load_client_project_script(scriptFile); + } else { + logger.warning() << "project client script does not exists: " << scriptFile.string(); + } +} + +void AppScriptsControl::onScreenChange(const std::string& name, bool show) { + if (clientScript) { + clientScript->onScreenChange(name, show); + } +} + +void AppScriptsControl::tick() { + if (scriptCoroutine && scriptCoroutine->isActive()) { + scriptCoroutine->update(); + } +} + +void AppScriptsControl::terminate(std::string_view reason) { + if (scriptCoroutine->isActive()) { + scriptCoroutine->terminate(); + logger.info() << "script has been terminated due to " << reason; + } +} diff --git a/src/devtools/AppScriptsControl.hpp b/src/devtools/AppScriptsControl.hpp new file mode 100644 index 000000000..fb081a986 --- /dev/null +++ b/src/devtools/AppScriptsControl.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include "interfaces/Process.hpp" + +#include +#include + +namespace scripting { + class IClientProjectScript; +} + +struct CoreParameters; +struct Project; + +class AppScriptsControl { +public: + AppScriptsControl(const CoreParameters& params, const Project& project); + + void tick(); + void loadProjectClientScript(); + void terminate(std::string_view reason); + + void onScreenChange(const std::string& name, bool show); +private: + const Project& project; + std::unique_ptr clientScript; + std::unique_ptr scriptCoroutine; +}; diff --git a/src/devtools/Project.cpp b/src/devtools/Project.cpp index 86c73640e..12a85dedc 100644 --- a/src/devtools/Project.cpp +++ b/src/devtools/Project.cpp @@ -2,6 +2,7 @@ #include "data/dv_util.hpp" #include "debug/Logger.hpp" +#include "engine/CoreParameters.hpp" #include "io/io.hpp" #include "io/path.hpp" #include "logic/scripting/scripting.hpp" @@ -40,26 +41,6 @@ void Project::deserialize(const dv::value& src) { } } -void Project::loadProjectClientScript() { - io::path scriptFile = "project:project_client.lua"; - if (io::exists(scriptFile)) { - logger.info() << "starting project client script"; - clientScript = scripting::load_client_project_script(scriptFile); - } else { - logger.warning() << "project client script does not exists"; - } -} - -void Project::loadProjectStartScript() { - io::path scriptFile = "project:start.lua"; - if (io::exists(scriptFile)) { - logger.info() << "starting project start script"; - setupCoroutine = scripting::start_app_script(scriptFile); - } else { - logger.warning() << "project start script does not exists"; - } -} - bool Permissions::has(const std::string& name) const { return permissions.find(name) != permissions.end(); } diff --git a/src/devtools/Project.hpp b/src/devtools/Project.hpp index 23aa11ca5..daeac7ca0 100644 --- a/src/devtools/Project.hpp +++ b/src/devtools/Project.hpp @@ -1,17 +1,12 @@ #pragma once +#include "interfaces/Serializable.hpp" + #include #include #include #include -#include "interfaces/Process.hpp" -#include "interfaces/Serializable.hpp" - -namespace scripting { - class IClientProjectScript; -} - struct Permissions { static inline std::string DEBUGGING = "debugging"; static inline std::string NETWORK = "network"; @@ -27,15 +22,10 @@ struct Project : Serializable { std::string name; std::string title; std::vector basePacks; - std::unique_ptr clientScript; - std::unique_ptr setupCoroutine; Permissions permissions; ~Project(); dv::value serialize() const override; void deserialize(const dv::value& src) override; - - void loadProjectClientScript(); - void loadProjectStartScript(); }; diff --git a/src/engine/Engine.cpp b/src/engine/Engine.cpp index 879d95f0b..10f8cee6d 100644 --- a/src/engine/Engine.cpp +++ b/src/engine/Engine.cpp @@ -13,6 +13,7 @@ #include "content/ContentControl.hpp" #include "core_defs.hpp" #include "debug/Logger.hpp" +#include "devtools/AppScriptsControl.hpp" #include "devtools/DebuggingServer.hpp" #include "devtools/Editor.hpp" #include "devtools/Project.hpp" @@ -191,10 +192,8 @@ void Engine::initialize(CoreParameters coreParameters) { audio::set_input_device(name == "auto" ? "" : name); })); - project->loadProjectStartScript(); - if (!params.headless) { - project->loadProjectClientScript(); - } + appScripts = std::make_unique(params, *project); + if (params.stdinCommands) { cmd::start_stdin_cmd_reader(*this); } @@ -263,9 +262,7 @@ void Engine::detachDebugger() { } void Engine::applicationTick() { - if (project->setupCoroutine && project->setupCoroutine->isActive()) { - project->setupCoroutine->update(); - } + appScripts->tick(); } void Engine::updateFrontend() { @@ -353,6 +350,7 @@ void Engine::close() { network.reset(); clearKeepedObjects(); project.reset(); + appScripts.reset(); scripting::close(); logger.info() << "scripting finished"; if (!params.headless) { @@ -387,8 +385,8 @@ void Engine::loadProject() { } void Engine::setScreen(std::shared_ptr screen) { - if (project->clientScript && this->screen) { - project->clientScript->onScreenChange(this->screen->getName(), false); + if (this->screen) { + appScripts->onScreenChange(this->screen->getName(), false); } // reset audio channels (stop all sources) audio::reset_channel(audio::get_channel_index("regular")); @@ -397,8 +395,8 @@ void Engine::setScreen(std::shared_ptr screen) { if (this->screen) { this->screen->onOpen(); } - if (project->clientScript && this->screen) { - project->clientScript->onScreenChange(this->screen->getName(), true); + if (this->screen) { + appScripts->onScreenChange(this->screen->getName(), true); window->setShouldRefresh(); } } diff --git a/src/engine/Engine.hpp b/src/engine/Engine.hpp index 4f726b700..169631214 100644 --- a/src/engine/Engine.hpp +++ b/src/engine/Engine.hpp @@ -12,6 +12,7 @@ class Assets; class AssetsLoader; class AssetsManagement; +class AppScriptsControl; class ContentControl; class EngineController; class EnginePaths; @@ -55,6 +56,7 @@ class Engine : public util::ObjectsKeeper { std::unique_ptr project; std::unique_ptr settingsHandler; std::unique_ptr assets; + std::unique_ptr appScripts; std::shared_ptr screen; std::unique_ptr content; std::unique_ptr controller; diff --git a/src/engine/Mainloop.cpp b/src/engine/Mainloop.cpp index d6ca17cb9..342f81aaf 100644 --- a/src/engine/Mainloop.cpp +++ b/src/engine/Mainloop.cpp @@ -9,6 +9,8 @@ #include "world/Level.hpp" #include "graphics/ui/GUI.hpp" #include "graphics/ui/elements/Container.hpp" +#include "logic/scripting/scripting.hpp" +#include "io/path.hpp" static debug::Logger logger("mainloop"); @@ -16,9 +18,11 @@ Mainloop::Mainloop(Engine& engine) : engine(engine) { } void Mainloop::run() { + const auto& coreParams = engine.getCoreParameters(); auto& time = engine.getTime(); auto& window = engine.getWindow(); auto& settings = engine.getSettings(); + double targetDelta = 1.0 / static_cast(coreParams.tps); engine.setLevelConsumer([this](auto level, int64_t localPlayer) { if (level == nullptr) { @@ -35,10 +39,13 @@ void Mainloop::run() { logger.info() << "starting menu screen"; engine.setScreen(std::make_shared(engine)); + + double testTimer = 0.0; logger.info() << "main loop started"; - while (!window.isShouldClose()){ - time.update(window.time()); + while (!window.isShouldClose()) { + testTimer += targetDelta; + time.update(coreParams.testMode ? testTimer : window.time()); engine.applicationTick(); engine.updateFrontend(); diff --git a/src/engine/ServerMainloop.cpp b/src/engine/ServerMainloop.cpp index 54698194e..7c9970e5e 100644 --- a/src/engine/ServerMainloop.cpp +++ b/src/engine/ServerMainloop.cpp @@ -1,13 +1,13 @@ #include "ServerMainloop.hpp" #include "Engine.hpp" -#include "logic/scripting/scripting.hpp" #include "logic/LevelController.hpp" #include "interfaces/Process.hpp" #include "debug/Logger.hpp" #include "world/Level.hpp" #include "world/World.hpp" #include "util/platform.hpp" +#include "devtools/Project.hpp" #include @@ -32,21 +32,12 @@ void ServerMainloop::run() { setLevel(std::move(level)); }); - auto process = scripting::start_app_script( - "script:" + coreParams.scriptFile.filename().u8string() - ); - double targetDelta = 1.0 / static_cast(coreParams.tps); double delta = targetDelta; auto begin = system_clock::now(); auto startupTime = begin; - while (process->isActive()) { - if (engine.isQuitSignal()) { - process->terminate(); - logger.info() << "script has been terminated due to quit signal"; - break; - } + while (engine.isQuitSignal()) { if (coreParams.testMode) { time.step(delta); } else { @@ -55,7 +46,6 @@ void ServerMainloop::run() { duration_cast(now - startupTime).count() / 1e6); delta = time.getDelta(); } - process->update(); if (controller) { controller->getLevel()->getWorld()->updateTimers(delta); controller->update(glm::min(delta, 0.2), false); From 0185ef68167a303c42a4015f2ab6fbaeee467f48 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 1 Jun 2026 18:01:44 +0300 Subject: [PATCH 006/217] fix audio input device --- src/audio/audio.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/audio/audio.cpp b/src/audio/audio.cpp index d6e32d089..b037addcb 100644 --- a/src/audio/audio.cpp +++ b/src/audio/audio.cpp @@ -173,6 +173,12 @@ void audio::initialize( logger.info() << "initializing NoAudio backend"; backend = NoAudio::create().release(); } + if (settings.inputDevice.get() == "auto") { + auto inputDevices = audio::get_input_devices_names(); + if (!inputDevices.empty()) { + settings.inputDevice.set(inputDevices.at(0)); + } + } struct { std::string name; NumberSetting* setting; @@ -210,7 +216,9 @@ void audio::initialize( ::input_device = backend->openInputDevice( settings.inputDevice.get(), 44100, 1, 16 ); - ::input_device->startCapture(); + if (::input_device != nullptr) { + ::input_device->startCapture(); + } } else { if (::input_device) { ::input_device->stopCapture(); From 5930ef8285d46c2f1c0c81ebb9207c139e6f48a8 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 2 Jun 2026 23:51:28 +0300 Subject: [PATCH 007/217] add events.remove --- res/modules/internal/events.lua | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/res/modules/internal/events.lua b/res/modules/internal/events.lua index 98ff780f0..1243034c8 100644 --- a/res/modules/internal/events.lua +++ b/res/modules/internal/events.lua @@ -14,6 +14,7 @@ function events.on(event, func) events.handlers[event] = {} end table.insert(events.handlers[event], func) + return func end function events.reset(event, func) @@ -24,6 +25,14 @@ function events.reset(event, func) end end +function events.remove(event, handler) + local handlers = events.handlers[event] + if not handlers then + return + end + table.remove_value(handlers, handler) +end + function events.remove_by_prefix(prefix) for name, handlers in pairs(events.handlers) do local actualname = name From bb135d33b5ab8dde0d71342a323d64424befc357 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 2 Jun 2026 23:57:22 +0300 Subject: [PATCH 008/217] add on_destroy layout event --- src/frontend/UiDocument.cpp | 16 ++++++++++++++++ src/frontend/UiDocument.hpp | 3 +++ src/logic/scripting/scripting.cpp | 18 ++++++++++++++++++ src/logic/scripting/scripting.hpp | 7 +++++-- 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/frontend/UiDocument.cpp b/src/frontend/UiDocument.cpp index 7e0c9bfc6..d0a158baf 100644 --- a/src/frontend/UiDocument.cpp +++ b/src/frontend/UiDocument.cpp @@ -7,6 +7,9 @@ #include "graphics/ui/elements/InventoryView.hpp" #include "graphics/ui/gui_xml.hpp" #include "logic/scripting/scripting.hpp" +#include "debug/Logger.hpp" + +static debug::Logger logger("ui-document"); UiDocument::UiDocument( std::string id, @@ -17,6 +20,19 @@ UiDocument::UiDocument( rebuildIndices(); } +UiDocument::~UiDocument() { + try { + scripting::on_ui_destroy(this); + } catch (const std::exception& err) { + logger.error() << "an error occurred on calling on_destroy event for document '" + << id << "': " << err.what(); + } catch (...) { + logger.error() << "unknown exception caught on calling on_destroy " + "event for document '" + << id << "'"; + } +} + void UiDocument::rebuildIndices() { map.clear(); gui::UINode::getIndices(root, map); diff --git a/src/frontend/UiDocument.hpp b/src/frontend/UiDocument.hpp index 5b40ca8ca..b2f0c9761 100644 --- a/src/frontend/UiDocument.hpp +++ b/src/frontend/UiDocument.hpp @@ -17,6 +17,7 @@ struct UiDocScript { bool onopen : 1; bool onprogress : 1; bool onclose : 1; + bool ondestroy : 1; }; using UINodesMap = std::unordered_map>; @@ -35,6 +36,8 @@ public: scriptenv env ); + ~UiDocument(); + void rebuildIndices(); void pushIndices(const std::shared_ptr& node); diff --git a/src/logic/scripting/scripting.cpp b/src/logic/scripting/scripting.cpp index 37597b058..67f8989c8 100644 --- a/src/logic/scripting/scripting.cpp +++ b/src/logic/scripting/scripting.cpp @@ -594,6 +594,9 @@ bool scripting::on_item_break_block( void scripting::on_ui_open( UiDocument* layout, std::vector args ) { + if (!layout->getScript().onopen) { + return; + } auto argsptr = std::make_shared>(std::move(args)); std::string name = layout->getId() + ".open"; @@ -608,6 +611,9 @@ void scripting::on_ui_open( void scripting::on_ui_progress( UiDocument* layout, int workDone, int workTotal ) { + if (!layout->getScript().onprogress){ + return; + } std::string name = layout->getId() + ".progress"; lua::emit_event(lua::get_main_state(), name, [=](auto L) { lua::pushinteger(L, workDone); @@ -617,12 +623,23 @@ void scripting::on_ui_progress( } void scripting::on_ui_close(UiDocument* layout, Inventory* inventory) { + if (!layout->getScript().onclose) { + return; + } std::string name = layout->getId() + ".close"; lua::emit_event(lua::get_main_state(), name, [inventory](auto L) { return lua::pushinteger(L, inventory ? inventory->getId() : 0); }); } +void scripting::on_ui_destroy(UiDocument* layout) { + if (!layout->getScript().ondestroy) { + return; + } + std::string name = layout->getId() + ".destroy"; + lua::emit_event(lua::get_main_state(), name, [](auto L) { return 0; }); +} + void scripting::on_scripts_loading() { auto L = lua::get_main_state(); @@ -810,6 +827,7 @@ void scripting::load_layout_script( script.onprogress = register_event(env, "on_progress", prefix + ".progress"); script.onclose = register_event(env, "on_close", prefix + ".close"); + script.ondestroy = register_event(env, "on_destroy", prefix + ".destroy"); } void scripting::close() { diff --git a/src/logic/scripting/scripting.hpp b/src/logic/scripting/scripting.hpp index 9d29528b8..68ac57486 100644 --- a/src/logic/scripting/scripting.hpp +++ b/src/logic/scripting/scripting.hpp @@ -152,14 +152,17 @@ namespace scripting { void on_attacked(const Entity& entity, Player* player, entityid_t attacker); void on_entity_used(const Entity& entity, Player* player); - /// @brief Called on UI view show + /// @brief Called on UI document show void on_ui_open(UiDocument* layout, std::vector args); void on_ui_progress(UiDocument* layout, int workDone, int totalWork); - /// @brief Called on UI view close + /// @brief Called on UI document close void on_ui_close(UiDocument* layout, Inventory* inventory); + /// @brief Called on UI document destroy + void on_ui_destroy(UiDocument* layout); + /// @brief Called on Content loading void on_scripts_loading(); From 52829023c2718876ce97bc91345b3cadfdd7d10e Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 2 Jun 2026 23:58:22 +0300 Subject: [PATCH 009/217] fix: duplicating core:chat event handlers in in-game chat --- res/layouts/ingame_chat.xml.lua | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/res/layouts/ingame_chat.xml.lua b/res/layouts/ingame_chat.xml.lua index dce629887..fdd535045 100644 --- a/res/layouts/ingame_chat.xml.lua +++ b/res/layouts/ingame_chat.xml.lua @@ -19,7 +19,7 @@ local function update_line(line, uptime) end end -events.on("core:chat", function(message) +local core_chat_handler = events.on("core:chat", function(message) while #lines >= max_lines do document[lines[1][1]]:destruct() table.remove(lines, 1) @@ -36,7 +36,7 @@ end) function on_open() if not initialized then initialized = true - + document.root:setInterval(1/animation_fps * 1000, function () local uptime = time.uptime() for _, line in ipairs(lines) do @@ -52,3 +52,7 @@ function on_open() end) end end + +function on_destroy() + events.remove("core:chat", core_chat_handler) +end From 089bcbbb64b8009db03fb9b6ed04c491e46d6933 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 3 Jun 2026 21:50:36 +0300 Subject: [PATCH 010/217] fix document events & breaking: document events removed from 'events' library temporary --- src/frontend/UiDocument.cpp | 2 +- src/frontend/hud.cpp | 4 +- src/frontend/menu.cpp | 8 +-- src/graphics/ui/elements/InlineFrame.cpp | 4 +- src/logic/scripting/lua/libs/libgui.cpp | 2 +- src/logic/scripting/scripting.cpp | 77 +++++++++++------------- src/logic/scripting/scripting.hpp | 8 +-- 7 files changed, 50 insertions(+), 55 deletions(-) diff --git a/src/frontend/UiDocument.cpp b/src/frontend/UiDocument.cpp index d0a158baf..4e8dbe72a 100644 --- a/src/frontend/UiDocument.cpp +++ b/src/frontend/UiDocument.cpp @@ -22,7 +22,7 @@ UiDocument::UiDocument( UiDocument::~UiDocument() { try { - scripting::on_ui_destroy(this); + scripting::on_ui_destroy(*this); } catch (const std::exception& err) { logger.error() << "an error occurred on calling on_destroy event for document '" << id << "': " << err.what(); diff --git a/src/frontend/hud.cpp b/src/frontend/hud.cpp index 4366d8c6e..1aa03436b 100644 --- a/src/frontend/hud.cpp +++ b/src/frontend/hud.cpp @@ -563,7 +563,7 @@ void Hud::add(const HudElement& element, const dv::value& argsArray) { args.emplace_back(static_cast(blockPos[i])); } scripting::on_ui_open( - element.getDocument(), + *element.getDocument(), std::move(args) ); } @@ -578,7 +578,7 @@ void Hud::onRemove(const HudElement& element) { if (invview) { inventory = invview->getInventory().get(); } - scripting::on_ui_close(document, inventory); + scripting::on_ui_close(*document, inventory); if (invview) { invview->unbind(); } diff --git a/src/frontend/menu.cpp b/src/frontend/menu.cpp index 372264a59..ab06b42dc 100644 --- a/src/frontend/menu.cpp +++ b/src/frontend/menu.cpp @@ -86,7 +86,7 @@ UiDocument* menus::show( ); auto document = documentPtr.get(); engine.getAssets()->store(std::move(documentPtr), fullname); - scripting::on_ui_open(document, std::move(args)); + scripting::on_ui_open(*document, std::move(args)); menu->addPage(name, document->getRoot()); menu->setPage(name); return document; @@ -99,13 +99,13 @@ void menus::show_process_panel( auto menu = engine.getGUI().getMenu(); menu->reset(); - auto doc = + auto document = menus::show(engine, "process", {util::wstr2str_utf8(langs::get(text))}); - std::dynamic_pointer_cast(doc->getRoot()) + std::dynamic_pointer_cast(document->getRoot()) ->listenInterval(0.01f, [=]() { task->update(); uint tasksDone = task->getWorkDone(); - scripting::on_ui_progress(doc, tasksDone, initialWork); + scripting::on_ui_progress(*document, tasksDone, initialWork); }); } diff --git a/src/graphics/ui/elements/InlineFrame.cpp b/src/graphics/ui/elements/InlineFrame.cpp index 50fc79762..319a16b1b 100644 --- a/src/graphics/ui/elements/InlineFrame.cpp +++ b/src/graphics/ui/elements/InlineFrame.cpp @@ -13,7 +13,7 @@ InlineFrame::~InlineFrame() = default; void InlineFrame::setSrc(const std::string& src) { this->src = src; if (document) { - scripting::on_ui_close(document.get(), nullptr); + scripting::on_ui_close(*document, nullptr); document = nullptr; root = nullptr; } @@ -31,7 +31,7 @@ void InlineFrame::setDocument(const std::shared_ptr& document) { root->setSize(size); gui.postRunnable([this]() { - scripting::on_ui_open(this->document.get(), {}); + scripting::on_ui_open(*this->document, {}); }); } diff --git a/src/logic/scripting/lua/libs/libgui.cpp b/src/logic/scripting/lua/libs/libgui.cpp index cdd3dad28..5ede0694c 100644 --- a/src/logic/scripting/lua/libs/libgui.cpp +++ b/src/logic/scripting/lua/libs/libgui.cpp @@ -1144,7 +1144,7 @@ static int l_gui_load_document(lua::State* L) { auto document = documentPtr.get(); engine->requireAssets().store(std::move(documentPtr), alias); - scripting::on_ui_open(document, {args}); + scripting::on_ui_open(*document, {args}); return 0; } diff --git a/src/logic/scripting/scripting.cpp b/src/logic/scripting/scripting.cpp index 67f8989c8..6954ee6b8 100644 --- a/src/logic/scripting/scripting.cpp +++ b/src/logic/scripting/scripting.cpp @@ -591,53 +591,46 @@ bool scripting::on_item_break_block( ); } -void scripting::on_ui_open( - UiDocument* layout, std::vector args -) { - if (!layout->getScript().onopen) { - return; - } - auto argsptr = - std::make_shared>(std::move(args)); - std::string name = layout->getId() + ".open"; - lua::emit_event(lua::get_main_state(), name, [=](auto L) { - for (const auto& value : *argsptr) { - lua::pushvalue(L, value); +static void call_layout_event(const UiDocument& layout, const std::vector& args, const std::string& eventName) { + auto L = lua::get_main_state(); + lua::pushenv(L, *layout.getEnvironment()); + if (lua::getfield(L, eventName)) { + for (const auto& arg : args) { + lua::pushvalue(L, arg); } - return argsptr->size(); - }); + lua::call_nothrow(L, args.size(), 0); + } + lua::pop(L); +} + +void scripting::on_ui_open( + const UiDocument& layout, std::vector args +) { + if (layout.getScript().onopen) { + call_layout_event(layout, args, "on_open"); + } } void scripting::on_ui_progress( - UiDocument* layout, int workDone, int workTotal + const UiDocument& layout, int workDone, int workTotal ) { - if (!layout->getScript().onprogress){ - return; + if (layout.getScript().onprogress){ + call_layout_event(layout, {workDone, workTotal}, "on_progress"); } - std::string name = layout->getId() + ".progress"; - lua::emit_event(lua::get_main_state(), name, [=](auto L) { - lua::pushinteger(L, workDone); - lua::pushinteger(L, workTotal); - return 2; - }); } -void scripting::on_ui_close(UiDocument* layout, Inventory* inventory) { - if (!layout->getScript().onclose) { - return; +void scripting::on_ui_close(const UiDocument& layout, Inventory* inventory) { + if (layout.getScript().onclose) { + call_layout_event( + layout, {inventory ? inventory->getId() : 0}, "on_close" + ); } - std::string name = layout->getId() + ".close"; - lua::emit_event(lua::get_main_state(), name, [inventory](auto L) { - return lua::pushinteger(L, inventory ? inventory->getId() : 0); - }); } -void scripting::on_ui_destroy(UiDocument* layout) { - if (!layout->getScript().ondestroy) { - return; +void scripting::on_ui_destroy(const UiDocument& layout) { + if (layout.getScript().ondestroy) { + call_layout_event(layout, {}, "on_destroy"); } - std::string name = layout->getId() + ".destroy"; - lua::emit_event(lua::get_main_state(), name, [](auto L) { return 0; }); } void scripting::on_scripts_loading() { @@ -822,12 +815,14 @@ void scripting::load_layout_script( ) { int env = *senv; - lua::pop(lua::get_main_state(), load_script(env, "layout", file, fileName)); - script.onopen = register_event(env, "on_open", prefix + ".open"); - script.onprogress = - register_event(env, "on_progress", prefix + ".progress"); - script.onclose = register_event(env, "on_close", prefix + ".close"); - script.ondestroy = register_event(env, "on_destroy", prefix + ".destroy"); + auto L = lua::get_main_state(); + lua::pop(L, load_script(env, "layout", file, fileName)); + lua::pushenv(L, env); + script.onopen = lua::hasfield(L, "on_open"); + script.onprogress = lua::hasfield(L, "on_progress"); + script.onclose = lua::hasfield(L, "on_close"); + script.ondestroy = lua::hasfield(L, "on_destroy"); + lua::pop(L); } void scripting::close() { diff --git a/src/logic/scripting/scripting.hpp b/src/logic/scripting/scripting.hpp index 68ac57486..ff2306589 100644 --- a/src/logic/scripting/scripting.hpp +++ b/src/logic/scripting/scripting.hpp @@ -153,15 +153,15 @@ namespace scripting { void on_entity_used(const Entity& entity, Player* player); /// @brief Called on UI document show - void on_ui_open(UiDocument* layout, std::vector args); + void on_ui_open(const UiDocument& layout, std::vector args); - void on_ui_progress(UiDocument* layout, int workDone, int totalWork); + void on_ui_progress(const UiDocument& layout, int workDone, int totalWork); /// @brief Called on UI document close - void on_ui_close(UiDocument* layout, Inventory* inventory); + void on_ui_close(const UiDocument& layout, Inventory* inventory); /// @brief Called on UI document destroy - void on_ui_destroy(UiDocument* layout); + void on_ui_destroy(const UiDocument& layout); /// @brief Called on Content loading void on_scripts_loading(); From 841c2a5d543fc53a8c0539c60420287c3d7d9bd6 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 4 Jun 2026 20:40:42 +0300 Subject: [PATCH 011/217] add file.remove_ext --- res/modules/internal/extensions/file.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/res/modules/internal/extensions/file.lua b/res/modules/internal/extensions/file.lua index 0f1040128..d308eda9c 100644 --- a/res/modules/internal/extensions/file.lua +++ b/res/modules/internal/extensions/file.lua @@ -23,6 +23,10 @@ function file.parent(path) return dir end +function file.remove_ext(path) + return string.format("%s/%s", file.parent(path), file.stem(path)) +end + function file.path(path) local pos = path:find(':') return path:sub(pos + 1) From 0bfc63d51fe55e36578f85af0e527bb4169f8764 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 4 Jun 2026 20:45:43 +0300 Subject: [PATCH 012/217] add debug.Logger lua class --- res/modules/internal/debugging.lua | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/res/modules/internal/debugging.lua b/res/modules/internal/debugging.lua index 1b56ee4b1..dd98a5c26 100644 --- a/res/modules/internal/debugging.lua +++ b/res/modules/internal/debugging.lua @@ -148,3 +148,29 @@ function error(message, level) end __error(message, level) end + +local LoggerClass = { + __index = { + info = function (self, text) + debug.log(string.format("[%s] %s", self.name, text)) + end, + warning = function (self, text) + debug.warning(string.format("[%s] %s", self.name, text)) + end, + error = function (self, text) + debug.error(string.format("[%s] %s", self.name, text)) + end + } +} + +local _getinfo = debug.getinfo + +function debug.Logger(name) + if not name then + local source = _getinfo(2, "S").source + name = file.remove_ext(source) + else + name = string.format("%s:%s", file.prefix(_getinfo(2, "S").source), name) + end + return setmetatable({name=name}, LoggerClass) +end From dd62393eb6b9ca8afdc1e5082c285a091d658532 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 4 Jun 2026 23:02:06 +0300 Subject: [PATCH 013/217] use real delta in client-side tests --- src/engine/Mainloop.cpp | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/engine/Mainloop.cpp b/src/engine/Mainloop.cpp index 342f81aaf..241fd6c2e 100644 --- a/src/engine/Mainloop.cpp +++ b/src/engine/Mainloop.cpp @@ -18,11 +18,9 @@ Mainloop::Mainloop(Engine& engine) : engine(engine) { } void Mainloop::run() { - const auto& coreParams = engine.getCoreParameters(); auto& time = engine.getTime(); auto& window = engine.getWindow(); auto& settings = engine.getSettings(); - double targetDelta = 1.0 / static_cast(coreParams.tps); engine.setLevelConsumer([this](auto level, int64_t localPlayer) { if (level == nullptr) { @@ -39,13 +37,10 @@ void Mainloop::run() { logger.info() << "starting menu screen"; engine.setScreen(std::make_shared(engine)); - - double testTimer = 0.0; logger.info() << "main loop started"; while (!window.isShouldClose()) { - testTimer += targetDelta; - time.update(coreParams.testMode ? testTimer : window.time()); + time.update(window.time()); engine.applicationTick(); engine.updateFrontend(); From b1311fd03cde0569c87f533863acff6976256f59 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 4 Jun 2026 23:21:58 +0300 Subject: [PATCH 014/217] fix test mode --- src/devtools/AppScriptsControl.cpp | 4 ++++ src/devtools/AppScriptsControl.hpp | 2 ++ src/engine/Engine.hpp | 4 ++++ src/engine/Mainloop.cpp | 20 ++++++++++++++------ src/engine/ServerMainloop.cpp | 3 ++- 5 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/devtools/AppScriptsControl.cpp b/src/devtools/AppScriptsControl.cpp index 16c3475d5..bb1a386a2 100644 --- a/src/devtools/AppScriptsControl.cpp +++ b/src/devtools/AppScriptsControl.cpp @@ -57,3 +57,7 @@ void AppScriptsControl::terminate(std::string_view reason) { logger.info() << "script has been terminated due to " << reason; } } + +bool AppScriptsControl::isFinished() const { + return scriptCoroutine == nullptr || !scriptCoroutine->isActive(); +} diff --git a/src/devtools/AppScriptsControl.hpp b/src/devtools/AppScriptsControl.hpp index fb081a986..4e44eef60 100644 --- a/src/devtools/AppScriptsControl.hpp +++ b/src/devtools/AppScriptsControl.hpp @@ -21,6 +21,8 @@ public: void terminate(std::string_view reason); void onScreenChange(const std::string& name, bool show); + + bool isFinished() const; private: const Project& project; std::unique_ptr clientScript; diff --git a/src/engine/Engine.hpp b/src/engine/Engine.hpp index 169631214..153acf4df 100644 --- a/src/engine/Engine.hpp +++ b/src/engine/Engine.hpp @@ -186,5 +186,9 @@ public: return debuggingServer.get(); } + AppScriptsControl& getAppScripts() { + return *appScripts; + } + void detachDebugger(); }; diff --git a/src/engine/Mainloop.cpp b/src/engine/Mainloop.cpp index 241fd6c2e..3a196f522 100644 --- a/src/engine/Mainloop.cpp +++ b/src/engine/Mainloop.cpp @@ -1,16 +1,17 @@ #include "Mainloop.hpp" -#include "Engine.hpp" #include "debug/Logger.hpp" +#include "devtools/AppScriptsControl.hpp" #include "devtools/Project.hpp" -#include "frontend/screens/MenuScreen.hpp" +#include "Engine.hpp" #include "frontend/screens/LevelScreen.hpp" +#include "frontend/screens/MenuScreen.hpp" +#include "graphics/ui/elements/Container.hpp" +#include "graphics/ui/GUI.hpp" +#include "io/path.hpp" +#include "logic/scripting/scripting.hpp" #include "window/Window.hpp" #include "world/Level.hpp" -#include "graphics/ui/GUI.hpp" -#include "graphics/ui/elements/Container.hpp" -#include "logic/scripting/scripting.hpp" -#include "io/path.hpp" static debug::Logger logger("mainloop"); @@ -21,6 +22,7 @@ void Mainloop::run() { auto& time = engine.getTime(); auto& window = engine.getWindow(); auto& settings = engine.getSettings(); + const auto& coreParams = engine.getCoreParameters(); engine.setLevelConsumer([this](auto level, int64_t localPlayer) { if (level == nullptr) { @@ -37,6 +39,8 @@ void Mainloop::run() { logger.info() << "starting menu screen"; engine.setScreen(std::make_shared(engine)); + + auto& appScripts = engine.getAppScripts(); logger.info() << "main loop started"; while (!window.isShouldClose()) { @@ -52,6 +56,10 @@ void Mainloop::run() { settings.display.adaptiveFpsInMenu.get() && dynamic_cast(engine.getScreen().get()) != nullptr ); + if (coreParams.testMode && appScripts.isFinished()) { + logger.info() << "test finished"; + engine.quit(); + } } logger.info() << "main loop stopped"; } diff --git a/src/engine/ServerMainloop.cpp b/src/engine/ServerMainloop.cpp index 7c9970e5e..c1a95330d 100644 --- a/src/engine/ServerMainloop.cpp +++ b/src/engine/ServerMainloop.cpp @@ -1,6 +1,7 @@ #include "ServerMainloop.hpp" #include "Engine.hpp" +#include "devtools/AppScriptsControl.hpp" #include "logic/LevelController.hpp" #include "interfaces/Process.hpp" #include "debug/Logger.hpp" @@ -37,7 +38,7 @@ void ServerMainloop::run() { auto begin = system_clock::now(); auto startupTime = begin; - while (engine.isQuitSignal()) { + while (!engine.isQuitSignal() && !engine.getAppScripts().isFinished()) { if (coreParams.testMode) { time.step(delta); } else { From 902c885fe0d72cbe5bd2ea9a95d4551516197d4a Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 5 Jun 2026 23:19:21 +0300 Subject: [PATCH 015/217] fix uinode.parent property --- src/logic/scripting/lua/libs/libgui.cpp | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/logic/scripting/lua/libs/libgui.cpp b/src/logic/scripting/lua/libs/libgui.cpp index 5ede0694c..057dc205e 100644 --- a/src/logic/scripting/lua/libs/libgui.cpp +++ b/src/logic/scripting/lua/libs/libgui.cpp @@ -420,15 +420,19 @@ static int p_get_data(UINode* node, lua::State* L) { return 0; } -static const std::string& request_node_id(const DocumentNode& docnode) { - std::string id = docnode.node->getId(); +static const std::string& request_node_id(UiDocument& document, UINode& node) { + const std::string& id = node.getId(); if (id.empty()) { - id = "#" + std::to_string( - reinterpret_cast(docnode.node.get())); + node.setId( "#" + std::to_string( + reinterpret_cast(&node))); + document.pushIndices(node.shared_from_this()); + return node.getId(); } - docnode.node->setId(std::move(id)); - docnode.document->pushIndices(docnode.node); - return docnode.node->getId(); + return id; +} + +static const std::string& request_node_id(const DocumentNode& docnode) { + return request_node_id(*docnode.document, *docnode.node); } /// @brief Push UI-document node object to stack @@ -443,15 +447,14 @@ static int push_document_node(lua::State* L, const std::string& id) { static int p_get_parent(UINode* node, lua::State* L) { auto parent = node->getParent(); - if (!parent) { + if (parent == nullptr) { return 0; } auto docname = lua::require_string(L, 1); auto element = lua::require_string(L, 2); auto docnode = get_document_node_impl(L, docname, element); - const auto& id = request_node_id(docnode); - + const auto& id = request_node_id(*docnode.document, *parent); return push_document_node(L, id); } From d2f272e403cf000810396e4760e9ee39c6e8b8b3 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 5 Jun 2026 23:22:44 +0300 Subject: [PATCH 016/217] add table.keys(t) --- res/modules/internal/extensions/table.lua | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/res/modules/internal/extensions/table.lua b/res/modules/internal/extensions/table.lua index d2d665755..f47eb643b 100644 --- a/res/modules/internal/extensions/table.lua +++ b/res/modules/internal/extensions/table.lua @@ -201,3 +201,11 @@ function table.tostring(t) end return s..']' end + +function table.keys(t) + local keys = {} + for key, _ in pairs(t) do + table.insert(keys, key) + end + return keys +end From 3429435ff509929a523ca2ea618dab04b50c682d Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 5 Jun 2026 23:25:10 +0300 Subject: [PATCH 017/217] add 'test' library for e2e tests purposes --- res/modules/internal/test.lua | 136 +++++++++++++++ res/scripts/stdlib.lua | 4 + src/logic/scripting/lua/libs/api_lua.hpp | 1 + src/logic/scripting/lua/libs/libtest.cpp | 45 +++++ src/logic/scripting/lua/lua_engine.cpp | 14 +- src/window/detail/GLFWWindow.cpp | 204 +++++++++++++---------- src/window/input.hpp | 5 + 7 files changed, 313 insertions(+), 96 deletions(-) create mode 100644 res/modules/internal/test.lua create mode 100644 src/logic/scripting/lua/libs/libtest.cpp diff --git a/res/modules/internal/test.lua b/res/modules/internal/test.lua new file mode 100644 index 000000000..a6ca7d096 --- /dev/null +++ b/res/modules/internal/test.lua @@ -0,0 +1,136 @@ +local __app = __vc_app + +local function get_node_center(node) + local pos = node.wpos + local size = node.size + return pos[1] + size[1] / 2, pos[2] + size[2] / 2 +end + +local function types_check_enum(value, enum) + if value == nil then + return true, nil + end + if type(value) ~= "string" then + return false, string.format("string expected [%s]", table.concat(table.keys(enum), "|")) + end + return true, nil +end + +local function types_check_number(value) + if value == nil then + return true, nil + end + local value_type = type(value) + if value_type ~= "number" then + return false, string.format("number expected, got %s", value_type) + end +end + +local function check_option(t, key, func, ...) + local value = t[key] + if value == nil then + return nil + end + local success, message = func(value, ...) + if not success then + error(string.format("invalid option '%s': %s", key, message)) + end +end + +local buttons_enum = { + left = 0, + right = 1, + middle = 2, +} +local no_options = {} + +local _tick = __app.tick +local _set_button_pressed = test.set_button_pressed +local _set_key_pressed = test.set_key_pressed +local _enter_text = test.enter_text +test.set_button_pressed = nil +test.set_key_pressed = nil +test.enter_text = nil + +function test.click(node, options) + options = options or no_options + + check_option(options, 'button', types_check_enum, buttons_enum) + check_option(options, 'timeout', types_check_number) + + local button = buttons_enum[options.button or 'left'] + local timeout = options.timeout or 1000 + + local tm = time.precise_time() + while (time.precise_time() - tm) * 1000 < timeout do + if node.exists then + break + end + _tick() + end + if not node.exists then + error(string.format("timeout %s ms exceeded: element %s does not exists", + math.floor(timeout), string.escape(node.id))) + end + + local center_x, center_y = get_node_center(node) + _set_button_pressed(button, center_x, center_y, true) + _tick() + _set_button_pressed(button, center_x, center_y, false) + _tick() +end + +-- TODO: replace with resolving in C++ side +function test.find_by_attr(node, attr, value) + if node[attr] == value then + return node + end + local i = 1 + while true do + local subnode = node[i] + if subnode == nil then + break + end + local found = test.find_by_attr(subnode, attr, value) + if found then + return found + end + i = i + 1 + end +end + +function test.find_by_text(node, text) + return test.find_by_attr(node, "text", text) +end + +function test.find_by_attr_presence(node, attr) + if node[attr] then + return node + end + local i = 1 + while true do + local subnode = node[i] + if subnode == nil then + break + end + local found = test.find_by_attr_presence(subnode, attr) + if found then + return found + end + i = i + 1 + end +end + +function test.fill(node, text, options) + test.click(node, options) + __app.tick() + _enter_text(text) + test.press("enter") +end + +function test.press(key) + _set_key_pressed(key, true) + __app.tick() + _set_key_pressed(key, false) + __app.tick() +end diff --git a/res/scripts/stdlib.lua b/res/scripts/stdlib.lua index 093501d75..a1949c1e6 100644 --- a/res/scripts/stdlib.lua +++ b/res/scripts/stdlib.lua @@ -127,6 +127,10 @@ require "core:internal/extensions/inventory" asserts = require "core:internal/asserts" events = require "core:internal/events" +if test then + require "core:internal/test" +end + function pack.unload(prefix) events.remove_by_prefix(prefix) end diff --git a/src/logic/scripting/lua/libs/api_lua.hpp b/src/logic/scripting/lua/libs/api_lua.hpp index e304d6dc1..123e1e532 100644 --- a/src/logic/scripting/lua/libs/api_lua.hpp +++ b/src/logic/scripting/lua/libs/api_lua.hpp @@ -44,6 +44,7 @@ extern const luaL_Reg posteffectslib[]; // gfx.posteffects extern const luaL_Reg quatlib[]; extern const luaL_Reg randomlib[]; extern const luaL_Reg compressionlib[]; +extern const luaL_Reg testlib[]; extern const luaL_Reg text3dlib[]; // gfx.text3d extern const luaL_Reg timelib[]; extern const luaL_Reg tomllib[]; diff --git a/src/logic/scripting/lua/libs/libtest.cpp b/src/logic/scripting/lua/libs/libtest.cpp new file mode 100644 index 000000000..d6675deeb --- /dev/null +++ b/src/logic/scripting/lua/libs/libtest.cpp @@ -0,0 +1,45 @@ +#include "api_lua.hpp" +#include "engine/Engine.hpp" +#include "window/input.hpp" + +#include + +using namespace scripting; + +static int l_set_button_pressed(lua::State* L) { + int button = lua::touinteger(L, 1); + int x = lua::tointeger(L, 2); + int y = lua::tointeger(L, 3); + bool pressed = lua::toboolean(L, 4); + + auto& input = engine->getInput(); + input.simulateCursorPos(x, y); + input.simulateClick(button, pressed); + return 0; +} + +static int l_set_key_pressed(lua::State* L) { + auto key = lua::require_string(L, 1); + bool pressed = lua::toboolean(L, 2); + + auto& input = engine->getInput(); + input.simulateKey(input_util::keycode_from(key), pressed); + return 0; +} + +static int l_enter_text(lua::State* L) { + auto string = lua::require_wstring(L, 1); + + auto& input = engine->getInput(); + for (auto chr : string) { + input.simulateCodepoint(chr); + } + return 0; +} + +const luaL_Reg testlib[] = { + {"set_button_pressed", lua::wrap}, + {"set_key_pressed", lua::wrap}, + {"enter_text", lua::wrap}, + {nullptr, nullptr} +}; diff --git a/src/logic/scripting/lua/lua_engine.cpp b/src/logic/scripting/lua/lua_engine.cpp index afb2f42eb..38b6a4b83 100644 --- a/src/logic/scripting/lua/lua_engine.cpp +++ b/src/logic/scripting/lua/lua_engine.cpp @@ -19,6 +19,7 @@ namespace { debug::Logger logger("lua-state"); lua::State* main_thread = nullptr; bool headless_mode = false; + bool test_mode = false; const std::unordered_map* project_args; } @@ -69,12 +70,12 @@ static void create_libs(State* L, StateType stateType) { openlib(L, "yaml", yamllib); openlib(L, "__vc_app", applib); - lua::getglobal(L, "__vc_app"); - lua::setregistry(L, "app"); + getglobal(L, "__vc_app"); + setregistry(L, "app"); if (stateType == StateType::SCRIPT) { - lua::getregistry(L, "app"); - lua::setglobal(L, "app"); + getregistry(L, "app"); + setglobal(L, "app"); } if (stateType == StateType::BASE || stateType == StateType::SCRIPT) { openlib(L, "assets", assetslib); @@ -99,6 +100,10 @@ static void create_libs(State* L, StateType stateType) { openlib(L, "__transform", transformlib); } + if (::test_mode) { + openlib(L, "test", testlib); + } + addfunc(L, "print", lua::wrap); addfunc(L, "crc32", lua::wrap); } @@ -191,6 +196,7 @@ void lua::initialize(const EnginePaths& paths, const CoreParameters& params) { logger.info() << LUAJIT_VERSION; headless_mode = params.headless; + test_mode = params.testMode; project_args = ¶ms.projectArgs; main_thread = create_state( paths, params.headless ? StateType::SCRIPT : StateType::BASE diff --git a/src/window/detail/GLFWWindow.cpp b/src/window/detail/GLFWWindow.cpp index db3d67241..6f42258d8 100644 --- a/src/window/detail/GLFWWindow.cpp +++ b/src/window/detail/GLFWWindow.cpp @@ -165,17 +165,127 @@ inline constexpr short MOUSE_KEYS_OFFSET = 1024; static GLFWcursor* standard_cursors[static_cast(CursorShape::LAST) + 1] = {}; -class GLFWInput : public Input { +class BaseInput : public Input { public: + std::vector codepoints; int scroll = 0; + + void onKeyCallback(int key, bool pressed) { + bool prevPressed = keys[key]; + keys[key] = pressed; + frames[key] = currentFrame; + if (pressed && !prevPressed) { + const auto& callbacks = keyCallbacks.find(static_cast(key)); + if (callbacks != keyCallbacks.end()) { + callbacks->second.notify(); + } + } + if (pressed && key < MOUSE_KEYS_OFFSET) { + pressedKeys.push_back(static_cast(key)); + } + } + + void onMouseCallback(int button, bool pressed) { + int key = button + MOUSE_KEYS_OFFSET; + onKeyCallback(key, pressed); + } + + bool isCursorLocked() const override { + return cursorLocked; + } + + void setCursorPosition(double xpos, double ypos) { + if (cursorDrag) { + delta.x += xpos - cursor.x; + delta.y += ypos - cursor.y; + } else { + cursorDrag = true; + } + cursor.x = xpos; + cursor.y = ypos; + } + + Bindings& getBindings() override { + return bindings; + } + + const Bindings& getBindings() const override { + return bindings; + } + + ObserverHandler addKeyCallback(Keycode key, KeyCallback callback) override { + return keyCallbacks[key].add(std::move(callback)); + } + + const std::vector& getPressedKeys() const override { + return pressedKeys; + } + + const std::vector& getCodepoints() const override { + return codepoints; + } + + CursorState getCursor() const override { + return {isCursorLocked(), cursor, delta}; + } + + int getScroll() override { + return scroll; + } + + bool pressed(Keycode key) const override { + int keycode = static_cast(key); + if (keycode < 0 || keycode >= KEYS_BUFFER_SIZE) { + return false; + } + return keys[keycode]; + } + bool jpressed(Keycode keycode) const override { + return pressed(keycode) && + frames[static_cast(keycode)] == currentFrame; + } + + bool clicked(Mousecode code) const override { + return pressed( + static_cast(MOUSE_KEYS_OFFSET + static_cast(code)) + ); + } + bool jclicked(Mousecode code) const override { + return clicked(code) && + frames[static_cast(code) + MOUSE_KEYS_OFFSET] == + currentFrame; + } + + void simulateKey(Keycode key, bool pressed) override { + onKeyCallback(static_cast(key), pressed); + } + + void simulateClick(int button, bool pressed) override { + onMouseCallback(static_cast(button), pressed); + } + + void simulateCursorPos(double xpos, double ypos) override { + setCursorPosition(xpos, ypos); + } + + void simulateCodepoint(uint codepoint) override { + codepoints.push_back(codepoint); + } +protected: uint currentFrame = 0; uint frames[KEYS_BUFFER_SIZE] {}; - std::vector codepoints; std::vector pressedKeys; Bindings bindings; bool keys[KEYS_BUFFER_SIZE] {}; std::unordered_map> keyCallbacks; + bool cursorLocked = false; + bool cursorDrag = false; + glm::vec2 delta {}; + glm::vec2 cursor {}; +}; +class GLFWInput : public BaseInput { +public: GLFWInput(GLFWwindow* window) : window(window) { } @@ -225,26 +335,6 @@ public: } } - void onKeyCallback(int key, bool pressed) { - bool prevPressed = keys[key]; - keys[key] = pressed; - frames[key] = currentFrame; - if (pressed && !prevPressed) { - const auto& callbacks = keyCallbacks.find(static_cast(key)); - if (callbacks != keyCallbacks.end()) { - callbacks->second.notify(); - } - } - if (pressed && key < MOUSE_KEYS_OFFSET) { - pressedKeys.push_back(static_cast(key)); - } - } - - void onMouseCallback(int button, bool pressed) { - int key = button + MOUSE_KEYS_OFFSET; - onKeyCallback(key, pressed); - } - const char* getClipboardText() const override { return glfwGetClipboardString(window); } @@ -253,41 +343,6 @@ public: glfwSetClipboardString(window, text); } - int getScroll() override { - return scroll; - } - - bool pressed(Keycode key) const override { - int keycode = static_cast(key); - if (keycode < 0 || keycode >= KEYS_BUFFER_SIZE) { - return false; - } - return keys[keycode]; - } - bool jpressed(Keycode keycode) const override { - return pressed(keycode) && - frames[static_cast(keycode)] == currentFrame; - } - - bool clicked(Mousecode code) const override { - return pressed( - static_cast(MOUSE_KEYS_OFFSET + static_cast(code)) - ); - } - bool jclicked(Mousecode code) const override { - return clicked(code) && - frames[static_cast(code) + MOUSE_KEYS_OFFSET] == - currentFrame; - } - - CursorState getCursor() const override { - return {isCursorLocked(), cursor, delta}; - } - - bool isCursorLocked() const override { - return cursorLocked; - } - void toggleCursor() override { cursorDrag = false; if (cursorLocked) { @@ -298,43 +353,8 @@ public: } cursorLocked = !cursorLocked; } - - void setCursorPosition(double xpos, double ypos) { - if (cursorDrag) { - delta.x += xpos - cursor.x; - delta.y += ypos - cursor.y; - } else { - cursorDrag = true; - } - cursor.x = xpos; - cursor.y = ypos; - } - - Bindings& getBindings() override { - return bindings; - } - - const Bindings& getBindings() const override { - return bindings; - } - - ObserverHandler addKeyCallback(Keycode key, KeyCallback callback) override { - return keyCallbacks[key].add(std::move(callback)); - } - - const std::vector& getPressedKeys() const override { - return pressedKeys; - } - - const std::vector& getCodepoints() const override { - return codepoints; - } private: GLFWwindow* window; - bool cursorLocked = false; - bool cursorDrag = false; - glm::vec2 delta {}; - glm::vec2 cursor {}; }; static_assert(!std::is_abstract()); diff --git a/src/window/input.hpp b/src/window/input.hpp index 84c120409..59d663672 100644 --- a/src/window/input.hpp +++ b/src/window/input.hpp @@ -274,6 +274,11 @@ public: virtual bool clicked(Mousecode mousecode) const = 0; virtual bool jclicked(Mousecode mousecode) const = 0; + virtual void simulateKey(Keycode key, bool pressed) = 0; + virtual void simulateClick(int button, bool pressed) = 0; + virtual void simulateCursorPos(double xpos, double ypos) = 0; + virtual void simulateCodepoint(uint codepoint) = 0; + virtual CursorState getCursor() const = 0; virtual bool isCursorLocked() const = 0; From 84b2aef81f82d46b07d9bd059540d7ac90cae3ec Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 8 Jun 2026 22:21:15 +0300 Subject: [PATCH 018/217] fix ui elements related things --- res/modules/internal/gui_util.lua | 19 +++++++++++-------- res/modules/internal/test.lua | 5 ++++- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/res/modules/internal/gui_util.lua b/res/modules/internal/gui_util.lua index 0ec47291e..656a0dfc8 100644 --- a/res/modules/internal/gui_util.lua +++ b/res/modules/internal/gui_util.lua @@ -77,15 +77,18 @@ function Element.new(docname, name) end -- the engine automatically creates an instance for every ui document (layout) -local Document = {} -function Document.new(docname) - return setmetatable({name=docname}, { - __index=function(self, k) - local elem = Element.new(self.name, k) - rawset(self, k, elem) - return elem +local Document = { + __index=function(self, k) + if type(k) ~= "string" then + error("element id is not a string") end - }) + local elem = Element.new(self.name, k) + rawset(self, k, elem) + return elem + end +} +function Document.new(docname) + return setmetatable({name=docname}, Document) end local RadioGroup = {} diff --git a/res/modules/internal/test.lua b/res/modules/internal/test.lua index a6ca7d096..ccabf3add 100644 --- a/res/modules/internal/test.lua +++ b/res/modules/internal/test.lua @@ -70,7 +70,7 @@ function test.click(node, options) end if not node.exists then error(string.format("timeout %s ms exceeded: element %s does not exists", - math.floor(timeout), string.escape(node.id))) + math.floor(timeout), string.escape(rawget(node, 'name')))) end local center_x, center_y = get_node_center(node) @@ -82,6 +82,9 @@ end -- TODO: replace with resolving in C++ side function test.find_by_attr(node, attr, value) + if getmetatable(node) == Document then + return test.find_by_attr(node.root, attr, value) + end if node[attr] == value then return node end From 958d0c7072988f0f3e772896ed17ae243f56377e Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 9 Jun 2026 19:13:07 +0300 Subject: [PATCH 019/217] update doc/ru/scripting/extensions.md --- doc/ru/scripting/extensions.md | 8 +++++++- res/modules/internal/extensions/table.lua | 6 +----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/doc/ru/scripting/extensions.md b/doc/ru/scripting/extensions.md index 868210056..922d02404 100644 --- a/doc/ru/scripting/extensions.md +++ b/doc/ru/scripting/extensions.md @@ -62,6 +62,12 @@ table.sub(arr: table, start: number | nil, stop: number | nil) -> table table.insert_unique(t: table, val: any) table.insert_unique(t: table, pos: int, val: any) +-- Возвращает таблицу, содержащую все ключи переданной таблицы, включая числовые. +table.keys(t: table) -> table + +-- Добавляет в таблицу t все пары ключ-значение из таблицы extension, при этом если в extension присутствует ключ из t, то его значение будет перезаписано. +table.extend(t: table, extension: table) -> table + -- Конвертирует переданную таблицу в строку. table.tostring(t: table) -> string ``` @@ -200,4 +206,4 @@ await(co: coroutine) -> result, error -- Константа, в которой хранится PID текущего инстанса движка. os.pid -> number -``` \ No newline at end of file +``` diff --git a/res/modules/internal/extensions/table.lua b/res/modules/internal/extensions/table.lua index f47eb643b..a96d78fe8 100644 --- a/res/modules/internal/extensions/table.lua +++ b/res/modules/internal/extensions/table.lua @@ -59,11 +59,7 @@ end function table.extend(t1, t2) for i, v in pairs(t2) do - if type(i) == "number" then - t1[#t1 + 1] = v - else - t1[i] = v - end + t1[i] = v end return t1 From 5a040893f1763e495456e2600d7bd580094bb8c8 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 9 Jun 2026 19:42:51 +0300 Subject: [PATCH 020/217] add doc/en/scripting/extensions.md --- doc/en/scripting.md | 1 + doc/en/scripting/extensions.md | 204 +++++++++++++++++++++++++++++++++ doc/ru/scripting/extensions.md | 2 +- 3 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 doc/en/scripting/extensions.md diff --git a/doc/en/scripting.md b/doc/en/scripting.md index a21b959ed..17e7f7a7a 100644 --- a/doc/en/scripting.md +++ b/doc/en/scripting.md @@ -42,6 +42,7 @@ Subsections: - [utf8](scripting/builtins/libutf8.md) - [vec2, vec3, vec4](scripting/builtins/libvecn.md) - [world](scripting/builtins/libworld.md) +- [Extensions for standard libraries](scripting/extensions.md) - [Module core:bit_converter](scripting/modules/core_bit_converter.md) - [Module core:data_buffer](scripting/modules/core_data_buffer.md) - [Module core:vector2, core:vector3](scripting/modules/core_vector2_vector3.md) diff --git a/doc/en/scripting/extensions.md b/doc/en/scripting/extensions.md new file mode 100644 index 000000000..94a55ef00 --- /dev/null +++ b/doc/en/scripting/extensions.md @@ -0,0 +1,204 @@ +# Standard Library Extensions + +The **stdmin.lua** kernel script defines functions that extend and complement some of the standard **Lua** libraries. + +## Contents: +- [table extensions](#table-extensions) +- [string extensions](#string-extensions) +- [math extensions](#math-extensions) +- [bit extensions](#bit-extensions) +- [additional global functions](#additional-global-functions) + +## Table Extensions +```lua +-- Creates and returns a copy of the given table by creating a new one and copying all elements from the given table into it. +table.copy(t: table) -> table + +-- The deep copy function creates a full copy of the source table, including all its subtables. +table.deep_copy(t: table) -> table + +-- Returns the number of pairs in the given table. +table.count_pairs(t: table) -> int + +-- Returns one element from the given table at a random position. +table.random(t: table) -> any + +-- Returns true if x is contained in t. +table.has(t: table, x: any) -> boolean + +-- Returns the index of x in t. If the given object is not contained in the table, the function returns -1. +table.index(t: table, x: any) -> int + +-- Removes element x from t. +table.remove_value(t: table, x: any) + +-- Shuffles values ​​in the table. +table.shuffle(t: table) -> table + +-- Adds values ​​from table t2 to table t1. If table t2 contains a key from t1, the key's value will not be changed. +table.merge(t1: table, t2: table) -> table + +-- Iterates through the table and applies a func to all its elements, returning the new value of the element. +table.map(t: table, func: function(indx, value)) -> table + +-- Iterates through the table using a func that returns true if the element should be kept and false if it should be deleted. +table.filter(t: table, func: function(indx, value)) -> table + +-- Allows you to safely retrieve the value for the specified key. If the key exists in the table, the method will return its value. +-- If the key is missing, the method will set it to the default value and return it. +table.set_default(t: table, key: int | string, default: any) -> any + +-- Returns a "flattened" version of the original table. +table.flat(t: table) -> table + +-- Returns a deep "flat" version of the original table. +table.deep_flat(t: table) -> table + +-- Returns a truncated version of the table from index start to index stop, inclusive. Key-value pairs +-- are not preserved in the new table. For nil values, the value starts at 1 and ends at #arr, respectively. +table.sub(arr: table, start: number | nil, stop: number | nil) -> table + +-- Adds a value to the table only if it was not originally there. +table.insert_unique(t: table, val: any) +table.insert_unique(t: table, pos: int, val: any) + +-- Returns a table containing all keys of the given table, including numeric ones. table.keys(t: table) -> table + +-- Adds all key-value pairs from table extension to table t. If a key from t is present in extension, its value will be overwritten. +table.extend(t: table, extension: table) -> table + +-- Converts the passed table to a string. +table.tostring(t: table) -> string +``` + +## String extensions + +It's important to note that all of the functions listed below that extend **string** can be used as meta-methods on string instances, i.e.: + +```lua +local str = "ABA str BAB" + +if str:starts_with("ABA") and str:ends_with("BAB") then +print(str:replace("BA", "DC")) +end +``` + +```lua +-- Splits the string into parts based on the specified separator/expression and returns the result as a table of strings. If withpattern is true, the separator parameter will be evaluated as a regular expression. string.explode(separator: string, str: string, withpattern: boolean) -> table + +-- Splits the string into parts based on the specified delimiter and returns the result as a table of strings. +string.split(str: string, delimiter: string) -> table + +-- Escapes special characters in the string, such as `()[]+-.$%^?*`, into `%character` format. The `NUL` character (`\0`) will be converted to `%z`. +string.pattern_safe(str: string) -> string + +-- Splits seconds into hours, minutes, and milliseconds and formats them using the following parameter order: `minutes, seconds, milliseconds`, and then returns the result. If format is not specified, it returns a table where: +-- h - hours, +-- m - minutes, +-- s - seconds, +-- ms - milliseconds. +string.formatted_time(seconds: number, format: string) -> string | table + +-- Replaces all substrings in str equal to tofind with toreplace and returns a string with all the modified substrings. +string.replace(str: string, tofind: string, toreplace: string) -> string + +-- Removes all characters equal to char from the string from the left and right ends and returns the result. +-- If the char parameter is undefined, all empty characters will be selected. +string.trim(str: string, char: string) -> string + +-- Removes all characters equal to char from the string from the left end and returns the result. +-- If the char parameter is undefined, all empty characters will be selected. +string.trim_left(str: string, char: string) -> string + +-- Removes all characters equal to char from the right end of the string and returns the result. +-- If the char parameter is undefined, all empty characters will be selected. +string.trim_right(str: string, char: string) -> string + +-- Returns true if the string begins with the substring start. +string.starts_with(str: string, start: string) -> boolean + +-- Returns true if the string ends with the substring endStr. +string.ends_with(str: string, endStr: string) -> boolean + +-- The string.lower and string.upper functions are also overridden by utf8.lower and utf8.upper. + +-- Escapes a string. It is an alias for utf8.escape. +string.escape(str: string) -> string + +-- Escapes special XML characters. An alias for utf8.escape_xml. +string.escape_xml(text: string) -> string + +-- Adds a char to the left and right of the string until its size equals size. +-- By default, char is equal to the space character. +string.pad(str: string, size: int, char: string) -> string + +-- Adds a char to the left of the string until its size equals size. +-- By default, char is equal to the space character. +string.left_pad(str: string, size: int, char: string) -> string + +-- Adds a char to the right of the string until its size equals size. +-- By default, char is equal to the space character. +string.right_pad(str: string, size: int, char: string) -> string +``` + +## Math extensions + +```lua +-- Returns _in if it is in the range low <= _in <= high +-- Otherwise, returns the boundary to which _in is closest. +math.clamp(_in: number, low: number, high: number) -> number + +-- Returns a random fractional number in the range low to high. +math.rand(low: number, high: number) -> number + +-- Returns the normalized value of num relative to conf. +math.normalize(num: number, [optional] conf: number) -> number + +-- Returns the rounded value of num to the specified number of decimal places. +math.round(num: number, [optional] places: number) -> number + +-- Returns the sum of all received arguments. If a table was passed as an argument, the method will return the sum of all its elements. +math.sum(x: number, ... | t: table) -> number + +``` + +## Bit extensions +```lua + +-- Common arguments: +-- * expr: A string containing a bitwise expression, conforming to the Lua 5.3 bitwise operations format +-- * args: A list of names of the expression arguments. If nil, the list is automatically generated based on the detected identifiers. + +- Compiles the function to perform bitwise operations +-- * asFunction: If true, returns the function; otherwise, returns a string of function code +bit.compile(expr: string, args: table | nil, asFunction: boolean=true) -> function | string + +-- Compiles the function to perform bitwise operations and executes it in place +-- * ...: Values ​​to be passed to the compiled function. bit.execute(expr: string, args: table | nil, ...) -> number +``` +## Additional Global Functions + +This script also defines other global functions that are available for use. Their list is below. + +```lua +-- Returns true if the passed table is an array, that is, if each key is an integer greater than or equal to one +-- and if each key follows the previous one. +is_array(x: table) -> boolean + +-- Splits the path into two parts and returns them: the entry point and the file path. +parse_path(path: string) -> string, string + +-- Calls the function func iters times, passing it the arguments ..., and then prints to the console the time in microseconds that has elapsed +-- since the call to timeit. timeit(iters: int, func: function, ...) + +-- Causes the coroutine to pause until the number of seconds specified in timesec has elapsed. +-- The function can only be used inside a coroutine. +sleep(timesec: number) + +-- Waits for the passed coroutine to complete, returning the control flow. The function can only be used inside a coroutine. +-- Returns values ​​similar to those returned by pcall. +await(co: coroutine) -> result, error + +-- A constant storing the PID of the current engine instance. +os.pid -> number +``` diff --git a/doc/ru/scripting/extensions.md b/doc/ru/scripting/extensions.md index 922d02404..54a19f723 100644 --- a/doc/ru/scripting/extensions.md +++ b/doc/ru/scripting/extensions.md @@ -7,7 +7,7 @@ - [расширения для string](#расширения-для-string) - [расширения для math](#расширения-для-math) - [расширения для bit](#расширения-для-bit) -- [Дополнительные глобальные функции](#дополнительные-глобальные-функции) +- [дополнительные глобальные функции](#дополнительные-глобальные-функции) ## Расширения для table ```lua From 4a11dd28ec9b7a0344c21a6fc4c3ab80c429eb50 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 9 Jun 2026 23:51:26 +0300 Subject: [PATCH 021/217] fix segfault on world.get_chunk_data if no world open --- src/logic/scripting/lua/libs/libworld.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/logic/scripting/lua/libs/libworld.cpp b/src/logic/scripting/lua/libs/libworld.cpp index c2d6be43c..3b3b4af59 100644 --- a/src/logic/scripting/lua/libs/libworld.cpp +++ b/src/logic/scripting/lua/libs/libworld.cpp @@ -129,6 +129,9 @@ static int l_get_generator(lua::State* L) { } static int l_get_chunk_data(lua::State* L) { + if (level == nullptr) { + throw std::runtime_error("world is not open"); + } int x = static_cast(lua::tointeger(L, 1)); int z = static_cast(lua::tointeger(L, 2)); const auto& chunk = level->chunks->getChunk(x, z); @@ -175,7 +178,7 @@ static void integrate_chunk_client(Chunk& chunk) { static int l_set_chunk_data(lua::State* L) { if (level == nullptr) { - throw std::runtime_error("no open world"); + throw std::runtime_error("world is not open"); } int x = static_cast(lua::tointeger(L, 1)); @@ -201,7 +204,7 @@ static int l_set_chunk_data(lua::State* L) { static int l_save_chunk_data(lua::State* L) { if (level == nullptr) { - throw std::runtime_error("no open world"); + throw std::runtime_error("world is not open"); } int x = static_cast(lua::tointeger(L, 1)); From 4cc6a8387242d1edfc47b6dae4e74a1ff15c1006 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 10 Jun 2026 01:28:23 +0300 Subject: [PATCH 022/217] speed up monospace fonts --- src/coders/vector_fonts.cpp | 1 + src/coders/vector_fonts.hpp | 6 ++++++ src/graphics/core/Font.cpp | 11 ++++++++++- src/graphics/core/Font.hpp | 2 ++ 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/coders/vector_fonts.cpp b/src/coders/vector_fonts.cpp index ad7706e6b..a2b6b55d0 100644 --- a/src/coders/vector_fonts.cpp +++ b/src/coders/vector_fonts.cpp @@ -30,6 +30,7 @@ namespace { public: FTFontFile(FT_Face face, util::Buffer buffer) : face(std::move(face)), buffer(std::move(buffer)) { + monospace = this->face->face_flags & FT_FACE_FLAG_FIXED_WIDTH; } ~FTFontFile() { diff --git a/src/coders/vector_fonts.hpp b/src/coders/vector_fonts.hpp index a28f3c5bf..689eb17de 100644 --- a/src/coders/vector_fonts.hpp +++ b/src/coders/vector_fonts.hpp @@ -19,6 +19,12 @@ namespace vector_fonts { virtual std::unique_ptr renderPage( int pageid, std::vector& glyphs, int size ) = 0; + + bool isMonospace() const { + return monospace; + } + protected: + bool monospace = true; }; void initialize(); diff --git a/src/graphics/core/Font.cpp b/src/graphics/core/Font.cpp index 3f0b4b69b..7aaa67e80 100644 --- a/src/graphics/core/Font.cpp +++ b/src/graphics/core/Font.cpp @@ -26,6 +26,11 @@ Font::Font( pages(std::move(pages)), glyphs(std::move(glyphs)), fontFile(std::move(fontFile)) { + if (this->fontFile.has_value()) { + if (auto fontPtr = this->fontFile->lock()) { + monospace = fontPtr->isMonospace(); + } + } } Font::~Font() = default; @@ -34,6 +39,10 @@ int Font::getYOffset() const { return yoffset; } +bool Font::isMonospace() const { + return monospace; +} + int Font::getLineHeight() const { return lineHeight; } @@ -53,7 +62,7 @@ bool Font::isPrintableChar(uint codepoint) const { int FontMetrics::calcWidth(std::wstring_view text, size_t offset, size_t length) const { auto font = this->font.has_value() ? this->font->lock() : nullptr; - if (font == nullptr) { + if (font == nullptr || font->isMonospace()) { return std::min(text.length() - offset, length) * _glyphInterval; } int totalWidth = 0; diff --git a/src/graphics/core/Font.hpp b/src/graphics/core/Font.hpp index e23c45ba8..1f0e3ce8c 100644 --- a/src/graphics/core/Font.hpp +++ b/src/graphics/core/Font.hpp @@ -39,6 +39,7 @@ public: ); ~Font(); + bool isMonospace() const; int getLineHeight() const; int getYOffset() const; @@ -94,6 +95,7 @@ private: int lineHeight; int yoffset; int glyphInterval; + bool monospace = true; std::vector> pages; std::vector glyphs; std::optional> fontFile; From 07702f9e57fd0e2fd2ebcae00e3f5f8c6ae3c885 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 10 Jun 2026 23:08:48 +0300 Subject: [PATCH 023/217] refactor descriptors_manager --- .../internal/stream_providers/file.lua | 13 +- src/logic/scripting/descriptors_manager.cpp | 104 ---------------- src/logic/scripting/descriptors_manager.hpp | 39 ------ src/logic/scripting/io_descriptors.cpp | 111 ++++++++++++++++++ src/logic/scripting/io_descriptors.hpp | 23 ++++ src/logic/scripting/lua/libs/libfile.cpp | 42 +++---- 6 files changed, 166 insertions(+), 166 deletions(-) delete mode 100644 src/logic/scripting/descriptors_manager.cpp delete mode 100644 src/logic/scripting/descriptors_manager.hpp create mode 100644 src/logic/scripting/io_descriptors.cpp create mode 100644 src/logic/scripting/io_descriptors.hpp diff --git a/res/modules/internal/stream_providers/file.lua b/res/modules/internal/stream_providers/file.lua index ec81a42c0..484e869e4 100644 --- a/res/modules/internal/stream_providers/file.lua +++ b/res/modules/internal/stream_providers/file.lua @@ -9,10 +9,19 @@ local lib = { close = file.__close_descriptor } +local __open_descriptor = file.__open_descriptor +file.__open_descriptor = nil +file.__read_descriptor = nil +file.__write_descriptor = nil +file.__seek_descriptor = nil +file.__flush_descriptor = nil +file.__has_descriptor = nil +file.__close_descriptor = nil + return function(path, mode) return io_stream.new( - file.__open_descriptor(path, mode), + __open_descriptor(path, mode), mode:find('b') ~= nil, lib ) -end \ No newline at end of file +end diff --git a/src/logic/scripting/descriptors_manager.cpp b/src/logic/scripting/descriptors_manager.cpp deleted file mode 100644 index f7458ccc5..000000000 --- a/src/logic/scripting/descriptors_manager.cpp +++ /dev/null @@ -1,104 +0,0 @@ -#include "logic/scripting/descriptors_manager.hpp" - -#include "debug/Logger.hpp" - -static debug::Logger logger("descriptors-manager"); - -namespace scripting { - - std::vector> descriptors_manager::descriptors; - - std::istream* descriptors_manager::get_input(int descriptor) { - if (!is_readable(descriptor)) - return nullptr; - - return descriptors[descriptor]->in.get(); - } - - std::ostream* descriptors_manager::get_output(int descriptor) { - if (!is_writeable(descriptor)) - return nullptr; - - return descriptors[descriptor]->out.get(); - } - - void descriptors_manager::flush(int descriptor) { - if (is_writeable(descriptor)) { - descriptors[descriptor]->out->flush(); - } - } - - bool descriptors_manager::has_descriptor(int descriptor) { - return is_readable(descriptor) || is_writeable(descriptor); - } - - bool descriptors_manager::is_readable(int descriptor) { - return descriptor >= 0 && descriptor < static_cast(descriptors.size()) - && descriptors[descriptor].has_value() - && descriptors[descriptor]->in != nullptr; - } - - bool descriptors_manager::is_writeable(int descriptor) { - return descriptor >= 0 && descriptor < static_cast(descriptors.size()) - && descriptors[descriptor].has_value() - && descriptors[descriptor]->out != nullptr; - } - - void descriptors_manager::close(int descriptor) { - if (descriptor >= 0 && descriptor < static_cast(descriptors.size())) { - if (descriptors[descriptor].has_value()) { - auto& desc = descriptors[descriptor].value(); - - if (desc.out) - desc.out->flush(); - - desc.in.reset(); - desc.out.reset(); - } - - descriptors[descriptor].reset(); - - descriptors[descriptor] = std::nullopt; - } - } - - int descriptors_manager::open_descriptor(const io::path& path, bool write, bool read) { - std::unique_ptr in; - std::unique_ptr out; - - try { - if (read) - in = io::read(path); - - if (write) - out = io::write(path); - } catch (const std::exception& e) { - logger.error() << "failed to open descriptor for " << path.string() - << ": " << e.what(); - - return -1; - } - - for (int i = 0; i < static_cast(descriptors.size()); ++i) { - if (!descriptors[i].has_value()) { - descriptors[i] = StreamDescriptor{ std::move(in), std::move(out) }; - return i; - } - } - - descriptors.emplace_back(StreamDescriptor{ std::move(in), std::move(out) }); - - return static_cast(descriptors.size() - 1); - } - - - void descriptors_manager::close_all_descriptors() { - for (int i = 0; i < static_cast(descriptors.size()); ++i) { - if (descriptors[i].has_value()) { - close(i); - } - } - - descriptors.clear(); - } -} \ No newline at end of file diff --git a/src/logic/scripting/descriptors_manager.hpp b/src/logic/scripting/descriptors_manager.hpp deleted file mode 100644 index ccd1b67a3..000000000 --- a/src/logic/scripting/descriptors_manager.hpp +++ /dev/null @@ -1,39 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -#include "io/io.hpp" - -namespace scripting { - - struct StreamDescriptor { - std::unique_ptr in; - std::unique_ptr out; - }; - - class descriptors_manager { - private: - static std::vector> descriptors; - - public: - static std::istream* get_input(int descriptor); - static std::ostream* get_output(int descriptor); - - static void flush(int descriptor); - - static bool has_descriptor(int descriptor); - - static bool is_readable(int descriptor); - static bool is_writeable(int descriptor); - - static void close(int descriptor); - static int open_descriptor(const io::path& path, bool write, bool read); - - static void close_all_descriptors(); - }; -} \ No newline at end of file diff --git a/src/logic/scripting/io_descriptors.cpp b/src/logic/scripting/io_descriptors.cpp new file mode 100644 index 000000000..e6fda04dc --- /dev/null +++ b/src/logic/scripting/io_descriptors.cpp @@ -0,0 +1,111 @@ +#include "logic/scripting/io_descriptors.hpp" + +#include "debug/Logger.hpp" +#include "io/io.hpp" + +#include +#include +#include +#include + +static debug::Logger logger("descriptors-manager"); + +using namespace scripting; + +namespace { + struct StreamDescriptor { + std::unique_ptr in; + std::unique_ptr out; + }; + std::vector> descriptors; +} + +std::istream* io_descriptors::get_input(int id) { + if (!is_readable(id)) { + return nullptr; + } + return ::descriptors[id]->in.get(); +} + +std::ostream* io_descriptors::get_output(int id) { + if (!is_writeable(id)) { + return nullptr; + } + return ::descriptors[id]->out.get(); +} + +void io_descriptors::flush(int id) { + if (is_writeable(id)) { + ::descriptors[id]->out->flush(); + } +} + +bool io_descriptors::has_descriptor(int id) { + return id >= 0 && id < static_cast(::descriptors.size()) && + ::descriptors[id].has_value() && + (::descriptors[id]->in != nullptr || + ::descriptors[id]->out != nullptr); +} + +bool io_descriptors::is_readable(int id) { + return id >= 0 && id < static_cast(::descriptors.size()) + && ::descriptors[id].has_value() + && ::descriptors[id]->in != nullptr; +} + +bool io_descriptors::is_writeable(int id) { + return id >= 0 && id < static_cast(::descriptors.size()) + && ::descriptors[id].has_value() + && ::descriptors[id]->out != nullptr; +} + +void io_descriptors::close(int id) { + if (!has_descriptor(id)) { + return; + } + auto& desc = ::descriptors[id].value(); + if (desc.out) { + desc.out->flush(); + } + desc.in.reset(); + desc.out.reset(); + ::descriptors[id] = std::nullopt; +} + +int io_descriptors::open_descriptor(const io::path& path, bool write, bool read) { + std::unique_ptr in; + std::unique_ptr out; + + try { + if (read) { + in = io::read(path); + } + if (write) { + out = io::write(path); + } + } catch (const std::exception& e) { + logger.error() << "failed to open descriptor for " << path.string() + << ": " << e.what(); + return -1; + } + + for (int i = 0; i < static_cast(descriptors.size()); ++i) { + if (!descriptors[i].has_value()) { + descriptors[i] = StreamDescriptor{ std::move(in), std::move(out) }; + return i; + } + } + + ::descriptors.emplace_back(StreamDescriptor{ std::move(in), std::move(out) }); + return static_cast(::descriptors.size() - 1); +} + + +void io_descriptors::close_all_descriptors() { + for (int i = 0; i < static_cast(::descriptors.size()); ++i) { + if (::descriptors[i].has_value()) { + close(i); + } + } + ::descriptors.clear(); +} diff --git a/src/logic/scripting/io_descriptors.hpp b/src/logic/scripting/io_descriptors.hpp new file mode 100644 index 000000000..0400a920a --- /dev/null +++ b/src/logic/scripting/io_descriptors.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include +#include + +#include "io/fwd.hpp" + +namespace scripting::io_descriptors { + std::istream* get_input(int id); + std::ostream* get_output(int id); + + void flush(int id); + + bool has_descriptor(int id); + + bool is_readable(int id); + bool is_writeable(int id); + + void close(int id); + int open_descriptor(const io::path& path, bool write, bool read); + + void close_all_descriptors(); +} diff --git a/src/logic/scripting/lua/libs/libfile.cpp b/src/logic/scripting/lua/libs/libfile.cpp index 26248379c..964c6d487 100644 --- a/src/logic/scripting/lua/libs/libfile.cpp +++ b/src/logic/scripting/lua/libs/libfile.cpp @@ -10,7 +10,7 @@ #include "util/stringutil.hpp" #include "api_lua.hpp" #include "../lua_engine.hpp" -#include "logic/scripting/descriptors_manager.hpp" +#include "logic/scripting/io_descriptors.hpp" namespace fs = std::filesystem; using namespace scripting; @@ -263,13 +263,13 @@ static int l_open_descriptor(lua::State* L) { std::vector buffer; if(wplusMode) { - int temp_descriptor = scripting::descriptors_manager::open_descriptor(path, false, true); + int temp_descriptor = io_descriptors::open_descriptor(path, false, true); if (temp_descriptor == -1) { throw std::runtime_error("failed to open descriptor for initial reading"); } - auto* in_stream = scripting::descriptors_manager::get_input(temp_descriptor); + auto* in_stream = io_descriptors::get_input(temp_descriptor); in_stream->seekg(0, std::ios::end); std::streamsize size = in_stream->tellg(); @@ -278,17 +278,17 @@ static int l_open_descriptor(lua::State* L) { buffer.resize(size); in_stream->read(buffer.data(), size); - scripting::descriptors_manager::close(temp_descriptor); + io_descriptors::close(temp_descriptor); } - int descriptor = scripting::descriptors_manager::open_descriptor(path, write, read); + int descriptor = io_descriptors::open_descriptor(path, write, read); if(descriptor == -1) { throw std::runtime_error("failed to open descriptor"); } if(wplusMode) { - auto* out_stream = scripting::descriptors_manager::get_output(descriptor); + auto* out_stream = io_descriptors::get_output(descriptor); out_stream->write(buffer.data(), buffer.size()); out_stream->flush(); } @@ -297,23 +297,23 @@ static int l_open_descriptor(lua::State* L) { } static int l_has_descriptor(lua::State* L) { - return lua::pushboolean(L, scripting::descriptors_manager::has_descriptor(lua::tointeger(L, 1))); + return lua::pushboolean(L, io_descriptors::has_descriptor(lua::tointeger(L, 1))); } static int l_read_descriptor(lua::State* L) { int descriptor = lua::tointeger(L, 1); - if (!scripting::descriptors_manager::has_descriptor(descriptor)) { + if (!io_descriptors::has_descriptor(descriptor)) { throw std::runtime_error("unknown descriptor"); } - if (!scripting::descriptors_manager::is_readable(descriptor)) { + if (!io_descriptors::is_readable(descriptor)) { throw std::runtime_error("descriptor is not readable"); } int maxlen = lua::tointeger(L, 2); - auto* stream = scripting::descriptors_manager::get_input(descriptor); + auto* stream = io_descriptors::get_input(descriptor); util::Buffer buffer(maxlen); @@ -327,17 +327,17 @@ static int l_read_descriptor(lua::State* L) { static int l_write_descriptor(lua::State* L) { int descriptor = lua::tointeger(L, 1); - if (!scripting::descriptors_manager::has_descriptor(descriptor)) { + if (!io_descriptors::has_descriptor(descriptor)) { throw std::runtime_error("unknown descriptor"); } - if (!scripting::descriptors_manager::is_writeable(descriptor)) { + if (!io_descriptors::is_writeable(descriptor)) { throw std::runtime_error("descriptor is not writeable"); } auto data = lua::bytearray_as_string(L, 2); - auto* stream = scripting::descriptors_manager::get_output(descriptor); + auto* stream = io_descriptors::get_output(descriptor); stream->write(data.data(), static_cast(data.size())); @@ -350,7 +350,7 @@ static int l_write_descriptor(lua::State* L) { static int l_seek_descriptor(lua::State* L) { int descriptor = lua::tointeger(L, 1); - if (!scripting::descriptors_manager::has_descriptor(descriptor)) { + if (!io_descriptors::has_descriptor(descriptor)) { throw std::runtime_error("unknown descriptor"); } @@ -371,7 +371,7 @@ static int l_seek_descriptor(lua::State* L) { throw std::runtime_error("invalid seek mode"); } - auto* stream = scripting::descriptors_manager::get_output(descriptor); + auto* stream = io_descriptors::get_output(descriptor); stream->seekp(lua::tointeger(L, 3), dir); @@ -385,31 +385,31 @@ static int l_seek_descriptor(lua::State* L) { static int l_flush_descriptor(lua::State* L) { int descriptor = lua::tointeger(L, 1); - if (!scripting::descriptors_manager::has_descriptor(descriptor)) { + if (!io_descriptors::has_descriptor(descriptor)) { throw std::runtime_error("unknown descriptor"); } - if (!scripting::descriptors_manager::is_writeable(descriptor)) { + if (!io_descriptors::is_writeable(descriptor)) { throw std::runtime_error("descriptor is not writeable"); } - scripting::descriptors_manager::flush(descriptor); + io_descriptors::flush(descriptor); return 0; } static int l_close_descriptor(lua::State* L) { int descriptor = lua::tointeger(L, 1); - if (!scripting::descriptors_manager::has_descriptor(descriptor)) { + if (!io_descriptors::has_descriptor(descriptor)) { throw std::runtime_error("unknown descriptor"); } - scripting::descriptors_manager::close(descriptor); + io_descriptors::close(descriptor); return 0; } static int l_close_all_descriptors(lua::State* L) { - scripting::descriptors_manager::close_all_descriptors(); + io_descriptors::close_all_descriptors(); return 0; } From ce0f90d21e6e8ba2da491e6e9ad18f7d7b679eb9 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 11 Jun 2026 00:17:31 +0300 Subject: [PATCH 024/217] feat: memory_istream seek support --- src/io/memory_istream.hpp | 52 +++++++++++++++++++++++++++++++++++++- test/io/memory_istream.cpp | 11 ++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/src/io/memory_istream.hpp b/src/io/memory_istream.hpp index 7c0aded64..84a0c703f 100644 --- a/src/io/memory_istream.hpp +++ b/src/io/memory_istream.hpp @@ -15,8 +15,33 @@ public: memory_streambuf(const memory_streambuf&) = delete; memory_streambuf& operator=(const memory_streambuf&) = delete; + pos_type seekoff(off_type off, std::ios_base::seekdir way, + std::ios_base::openmode mode = std::ios_base::in) override { + if (!(mode & std::ios_base::in)) return pos_type(off_type(-1)); + char* base = eback(); + char* end = egptr(); + if (!base || !end) return pos_type(off_type(-1)); + + std::ptrdiff_t current = gptr() - base; + std::ptrdiff_t newpos; + if (way == std::ios_base::beg) newpos = off; + else if (way == std::ios_base::cur) newpos = current + off; + else if (way == std::ios_base::end) newpos = (end - base) + off; + else return pos_type(off_type(-1)); + + if (newpos < 0 || base + newpos > end) return pos_type(off_type(-1)); + setg(base, base + newpos, end); + return pos_type(off_type(newpos)); + } + + pos_type seekpos(pos_type sp, std::ios_base::openmode mode = std::ios_base::in) override { + return seekoff(off_type(sp), std::ios_base::beg, mode); + } protected: int_type underflow() override { + if (gptr() < egptr()) { + return traits_type::to_int_type(*gptr()); + } return traits_type::eof(); } @@ -36,7 +61,7 @@ private: class memory_view_streambuf : public std::streambuf { public: explicit memory_view_streambuf(const util::Buffer& buffer) - : buffer(std::move(buffer)) { + : buffer(buffer) { char* base = const_cast(this->buffer.data()); char* end = base + this->buffer.size(); setg(base, base, end); @@ -45,8 +70,33 @@ public: memory_view_streambuf(const memory_view_streambuf&) = delete; memory_view_streambuf& operator=(const memory_view_streambuf&) = delete; + pos_type seekoff(off_type off, std::ios_base::seekdir way, + std::ios_base::openmode mode = std::ios_base::in) override { + if (!(mode & std::ios_base::in)) return pos_type(off_type(-1)); + char* base = eback(); + char* end = egptr(); + if (!base || !end) return pos_type(off_type(-1)); + + std::ptrdiff_t current = gptr() - base; + std::ptrdiff_t newpos; + if (way == std::ios_base::beg) newpos = off; + else if (way == std::ios_base::cur) newpos = current + off; + else if (way == std::ios_base::end) newpos = (end - base) + off; + else return pos_type(off_type(-1)); + + if (newpos < 0 || base + newpos > end) return pos_type(off_type(-1)); + setg(base, base + newpos, end); + return pos_type(off_type(newpos)); + } + + pos_type seekpos(pos_type sp, std::ios_base::openmode mode = std::ios_base::in) override { + return seekoff(off_type(sp), std::ios_base::beg, mode); + } protected: int_type underflow() override { + if (gptr() < egptr()) { + return traits_type::to_int_type(*gptr()); + } return traits_type::eof(); } diff --git a/test/io/memory_istream.cpp b/test/io/memory_istream.cpp index faabd23cd..674d250fd 100644 --- a/test/io/memory_istream.cpp +++ b/test/io/memory_istream.cpp @@ -16,4 +16,15 @@ TEST(io, memory_istream) { ASSERT_EQ(text, std::string(data)); stream.read(text.data(), 1); ASSERT_TRUE(stream.eof()); + + // seek + stream.clear(); + stream.seekg(0); + ASSERT_TRUE(stream.good()); + stream.read(text.data(), n); + ASSERT_EQ(text, std::string(data)); + stream.seekg(-6, std::ios_base::cur); + ASSERT_TRUE(stream.good()); + stream.read(text.data(), 6); + ASSERT_EQ(text.substr(0, 6), "world!"); } From 0d2a0b012deda064e164bb581f84fef84523714d Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 11 Jun 2026 00:27:15 +0300 Subject: [PATCH 025/217] add io_stream:tell & fix io_stream:seek --- .../internal/stream_providers/file.lua | 2 + res/modules/io_stream.lua | 6 +- src/logic/scripting/io_descriptors.cpp | 34 +++++++-- src/logic/scripting/io_descriptors.hpp | 3 + src/logic/scripting/lua/libs/libfile.cpp | 74 ++++++++++--------- 5 files changed, 77 insertions(+), 42 deletions(-) diff --git a/res/modules/internal/stream_providers/file.lua b/res/modules/internal/stream_providers/file.lua index 484e869e4..840ff93f2 100644 --- a/res/modules/internal/stream_providers/file.lua +++ b/res/modules/internal/stream_providers/file.lua @@ -4,6 +4,7 @@ local lib = { read = file.__read_descriptor, write = file.__write_descriptor, seek = file.__seek_descriptor, + tell = file.__tell_descriptor, flush = file.__flush_descriptor, is_alive = file.__has_descriptor, close = file.__close_descriptor @@ -14,6 +15,7 @@ file.__open_descriptor = nil file.__read_descriptor = nil file.__write_descriptor = nil file.__seek_descriptor = nil +file.__tell_descriptor = nil file.__flush_descriptor = nil file.__has_descriptor = nil file.__close_descriptor = nil diff --git a/res/modules/io_stream.lua b/res/modules/io_stream.lua index b608a22a9..8a4507a3b 100644 --- a/res/modules/io_stream.lua +++ b/res/modules/io_stream.lua @@ -373,6 +373,10 @@ function io_stream:seek(mode, offset) self.ioLib.seek(self.descriptor, mode, offset) end +function io_stream:tell() + return self.ioLib.tell(self.descriptor) +end + function io_stream:is_alive() return self.ioLib.is_alive(self.descriptor) end @@ -399,4 +403,4 @@ function io_stream:flush() if self.flushMode ~= FLUSH_MODE_ONLY_BUFFER then self.ioLib.flush(self.descriptor) end end -return io_stream \ No newline at end of file +return io_stream diff --git a/src/logic/scripting/io_descriptors.cpp b/src/logic/scripting/io_descriptors.cpp index e6fda04dc..132eea0eb 100644 --- a/src/logic/scripting/io_descriptors.cpp +++ b/src/logic/scripting/io_descriptors.cpp @@ -14,6 +14,7 @@ using namespace scripting; namespace { struct StreamDescriptor { + // TODO: std::iostream? std::unique_ptr in; std::unique_ptr out; }; @@ -34,6 +35,31 @@ std::ostream* io_descriptors::get_output(int id) { return ::descriptors[id]->out.get(); } +static StreamDescriptor& require_descriptor(int id) { + if (!io_descriptors::has_descriptor(id)) { + throw std::runtime_error( + "io-descriptor with id " + std::to_string(id) + " does not exists" + ); + } + return *::descriptors[id]; +} + +std::istream& io_descriptors::require_input(int id) { + const auto& descriptor = require_descriptor(id); + if (descriptor.in) { + return *descriptor.in; + } + throw std::runtime_error("io-descriptor is not readable"); +} + +std::ostream& io_descriptors::require_output(int id) { + const auto& descriptor = require_descriptor(id); + if (descriptor.out) { + return *descriptor.out; + } + throw std::runtime_error("io-descriptor is not writeable"); +} + void io_descriptors::flush(int id) { if (is_writeable(id)) { ::descriptors[id]->out->flush(); @@ -42,15 +68,11 @@ void io_descriptors::flush(int id) { bool io_descriptors::has_descriptor(int id) { return id >= 0 && id < static_cast(::descriptors.size()) && - ::descriptors[id].has_value() && - (::descriptors[id]->in != nullptr || - ::descriptors[id]->out != nullptr); + ::descriptors[id].has_value(); } bool io_descriptors::is_readable(int id) { - return id >= 0 && id < static_cast(::descriptors.size()) - && ::descriptors[id].has_value() - && ::descriptors[id]->in != nullptr; + return has_descriptor(id) && ::descriptors[id]->in != nullptr; } bool io_descriptors::is_writeable(int id) { diff --git a/src/logic/scripting/io_descriptors.hpp b/src/logic/scripting/io_descriptors.hpp index 0400a920a..7e6207791 100644 --- a/src/logic/scripting/io_descriptors.hpp +++ b/src/logic/scripting/io_descriptors.hpp @@ -9,6 +9,9 @@ namespace scripting::io_descriptors { std::istream* get_input(int id); std::ostream* get_output(int id); + std::istream& require_input(int id); + std::ostream& require_output(int id); + void flush(int id); bool has_descriptor(int id); diff --git a/src/logic/scripting/lua/libs/libfile.cpp b/src/logic/scripting/lua/libs/libfile.cpp index 964c6d487..1a09e9962 100644 --- a/src/logic/scripting/lua/libs/libfile.cpp +++ b/src/logic/scripting/lua/libs/libfile.cpp @@ -302,46 +302,26 @@ static int l_has_descriptor(lua::State* L) { static int l_read_descriptor(lua::State* L) { int descriptor = lua::tointeger(L, 1); - - if (!io_descriptors::has_descriptor(descriptor)) { - throw std::runtime_error("unknown descriptor"); - } - - if (!io_descriptors::is_readable(descriptor)) { - throw std::runtime_error("descriptor is not readable"); - } - int maxlen = lua::tointeger(L, 2); - auto* stream = io_descriptors::get_input(descriptor); + auto& stream = io_descriptors::require_input(descriptor); + if (stream.eof()) { + stream.clear(); + } util::Buffer buffer(maxlen); - - stream->read(buffer.data(), maxlen); - - std::streamsize read_len = stream->gcount(); - + stream.read(buffer.data(), maxlen); + std::streamsize read_len = stream.gcount(); return lua::create_bytearray(L, buffer.data(), read_len); } static int l_write_descriptor(lua::State* L) { int descriptor = lua::tointeger(L, 1); - - if (!io_descriptors::has_descriptor(descriptor)) { - throw std::runtime_error("unknown descriptor"); - } - - if (!io_descriptors::is_writeable(descriptor)) { - throw std::runtime_error("descriptor is not writeable"); - } - auto data = lua::bytearray_as_string(L, 2); - auto* stream = io_descriptors::get_output(descriptor); - - stream->write(data.data(), static_cast(data.size())); - - if (!stream->good()) { + auto& stream = io_descriptors::require_output(descriptor); + stream.write(data.data(), static_cast(data.size())); + if (!stream.good()) { throw std::runtime_error("failed to write to stream"); } return 0; @@ -354,8 +334,9 @@ static int l_seek_descriptor(lua::State* L) { throw std::runtime_error("unknown descriptor"); } - std::string mode = lua::require_string(L, 2); + auto mode = lua::require_string(L, 2); std::ios_base::seekdir dir; + auto position = lua::tointeger(L, 3); switch (mode[0]) { case 'b': @@ -371,15 +352,37 @@ static int l_seek_descriptor(lua::State* L) { throw std::runtime_error("invalid seek mode"); } - auto* stream = io_descriptors::get_output(descriptor); + if (io_descriptors::is_writeable(descriptor)) { + auto& stream = io_descriptors::require_output(descriptor); + stream.seekp(position, dir); + if (!stream.good()) { + throw std::runtime_error("failed to seek stream"); + } + } + if (io_descriptors::is_readable(descriptor)) { + auto& stream = io_descriptors::require_input(descriptor); + stream.seekg(position, dir); + if (!stream.good()) { + throw std::runtime_error("failed to seek stream"); + } + } + return 0; +} - stream->seekp(lua::tointeger(L, 3), dir); +static int l_tell_descriptor(lua::State* L) { + int descriptor = lua::tointeger(L, 1); - if (!stream->good()) { - throw std::runtime_error("failed to seek stream"); + if (!io_descriptors::has_descriptor(descriptor)) { + throw std::runtime_error("unknown descriptor"); } - return 0; + if (io_descriptors::is_writeable(descriptor)) { + auto& stream = io_descriptors::require_output(descriptor); + return lua::pushinteger(L, stream.tellp()); + } else { + auto& stream = io_descriptors::require_input(descriptor); + return lua::pushinteger(L, stream.tellg()); + } } static int l_flush_descriptor(lua::State* L) { @@ -442,6 +445,7 @@ const luaL_Reg filelib[] = { {"__read_descriptor", lua::wrap}, {"__write_descriptor", lua::wrap}, {"__seek_descriptor", lua::wrap}, + {"__tell_descriptor", lua::wrap}, {"__flush_descriptor", lua::wrap}, {"__close_descriptor", lua::wrap}, {"__close_all_descriptors", lua::wrap}, From 34af3523d957b664f0f8800a5cc2fd34df389cba Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 11 Jun 2026 00:29:01 +0300 Subject: [PATCH 026/217] add io_stream lua test --- dev/tests/io_stream.lua | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 dev/tests/io_stream.lua diff --git a/dev/tests/io_stream.lua b/dev/tests/io_stream.lua new file mode 100644 index 000000000..d08c68b0c --- /dev/null +++ b/dev/tests/io_stream.lua @@ -0,0 +1,34 @@ +local device = file.create_memory_device() +file.write(device..":test.txt", "Hello\nWorld") +file.write_bytes(device..":test.bin", Bytearray({20, 30, 100, 200, 255})) + +local stream = file.open(device..":test.txt", 'r') + +local line1 = stream:read_line() +local line2 = stream:read_line() + +assert(line1 == "Hello") +assert(line2 == "World") + +local stream2 = file.open(device..":test.bin", 'rb') +local data = stream2:read(5) +assert(data[1] == 20) +assert(data[2] == 30) +assert(data[3] == 100) +assert(data[4] == 200) +assert(data[5] == 255) + +local stream3 = file.open(device..":test.txt", 'r') +assert(stream3:is_alive()) +stream3:close() +assert(not stream3:is_alive()) + +local stream4 = file.open(device..":test.txt", 'r') +stream4:seek('b', 6) +local line = stream4:read_line() +assert(line == "World") + +local stream5 = file.open(device..":test.txt", 'r') +stream5:seek('e', 0) +local pos = stream5:tell() +assert(pos == 11) From 3fc109721764c383db1a7eb54c6c5485fbdbbda4 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 18 Jun 2026 21:27:28 +0300 Subject: [PATCH 027/217] update WorldRenderer header --- src/graphics/render/WorldRenderer.hpp | 47 ++++++++++++++------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/src/graphics/render/WorldRenderer.hpp b/src/graphics/render/WorldRenderer.hpp index c3e6b1669..d472b7b75 100644 --- a/src/graphics/render/WorldRenderer.hpp +++ b/src/graphics/render/WorldRenderer.hpp @@ -44,6 +44,30 @@ struct CompileTimeShaderSettings { }; class WorldRenderer { +public: + static bool showChunkBorders; + static bool showEntitiesDebug; + + WorldRenderer(Engine& engine, LevelFrontend& frontend, Player& player); + ~WorldRenderer(); + + void update(const Camera& camera, float delta); + + void renderFrame( + const DrawContext& context, + Camera& camera, + bool hudVisible, + PostProcessing& postProcessing + ); + + void clear(); + + void setDebug(bool flag); + + void toggleLightsDebug(); + + Weather& getWeather(); +private: Engine& engine; const Level& level; Player& player; @@ -106,27 +130,4 @@ public: std::unique_ptr texts; std::unique_ptr blockWraps; std::unique_ptr skeletons; - - static bool showChunkBorders; - static bool showEntitiesDebug; - - WorldRenderer(Engine& engine, LevelFrontend& frontend, Player& player); - ~WorldRenderer(); - - void update(const Camera& camera, float delta); - - void renderFrame( - const DrawContext& context, - Camera& camera, - bool hudVisible, - PostProcessing& postProcessing - ); - - void clear(); - - void setDebug(bool flag); - - void toggleLightsDebug(); - - Weather& getWeather(); }; From af71d5b674a7454ca1be77e0af2f2fda90957564 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 18 Jun 2026 22:12:22 +0300 Subject: [PATCH 028/217] simplify settings-based shaders recompilation --- src/graphics/core/Shader.cpp | 3 +- src/graphics/render/WorldRenderer.cpp | 99 ++++++++++++++------------- src/graphics/render/WorldRenderer.hpp | 13 ++-- 3 files changed, 58 insertions(+), 57 deletions(-) diff --git a/src/graphics/core/Shader.cpp b/src/graphics/core/Shader.cpp index 8cc6f2025..1c752ef5a 100644 --- a/src/graphics/core/Shader.cpp +++ b/src/graphics/core/Shader.cpp @@ -186,7 +186,8 @@ void Shader::recompile(const std::vector& defines) { glDeleteProgram(id); id = newProgram; uniformLocations.clear(); - logger.info() << "shader " << id << " has been recompiled"; + logger.debug() << "shader program (" << vertexSource.file << ", " + << fragmentSource.file << ") has been recompiled"; } std::unique_ptr Shader::create( diff --git a/src/graphics/render/WorldRenderer.cpp b/src/graphics/render/WorldRenderer.cpp index 57cc90f24..e59d4392f 100644 --- a/src/graphics/render/WorldRenderer.cpp +++ b/src/graphics/render/WorldRenderer.cpp @@ -52,6 +52,7 @@ #include "world/Level.hpp" #include "world/LevelEvents.hpp" #include "world/World.hpp" +#include "debug/Logger.hpp" #include #include @@ -62,9 +63,20 @@ inline constexpr size_t BATCH3D_CAPACITY = 4096; inline constexpr size_t MODEL_BATCH_CAPACITY = 20'000; +static debug::Logger logger("world-renderer"); + bool WorldRenderer::showChunkBorders = false; bool WorldRenderer::showEntitiesDebug = false; +template +static ObserverHandler observe_setting( + ObservableSetting& setting, bool& dirtyShaders +) { + return setting.observe([&dirtyShaders](auto const&) { + dirtyShaders = true; + }); +} + WorldRenderer::WorldRenderer( Engine& engine, LevelFrontend& frontend, Player& player ) @@ -120,10 +132,41 @@ WorldRenderer::WorldRenderer( shadowMapping = std::make_unique(level); debugLines = std::make_unique(level); cloudsRenderer = std::make_unique(); + + keepAlive(observe_setting(settings.graphics.advancedRender, dirtyShaders)); + keepAlive(observe_setting(settings.graphics.shadowsQuality, dirtyShaders)); + keepAlive(observe_setting(settings.graphics.ssao, dirtyShaders)); } WorldRenderer::~WorldRenderer() = default; +void WorldRenderer::refreshSettings() { + Shader* affectedShaders[] { + &assets.require("main"), + &assets.require("entity"), + &assets.require("clouds"), + &assets.require("translucent"), + &assets.require("deferred_lighting").getShader(), + nullptr + }; + + const auto& graphics = engine.getSettings().graphics; + gbufferPipeline = graphics.advancedRender.get(); + + int shadowsQuality = graphics.shadowsQuality.get() * gbufferPipeline; + shadowMapping->setQuality(shadowsQuality); + + std::vector defines; + if (shadowsQuality != 0) defines.emplace_back("ENABLE_SHADOWS"); + if (graphics.ssao.get()) defines.emplace_back("ENABLE_SSAO"); + if (gbufferPipeline) defines.emplace_back("ADVANCED_RENDER"); + + logger.info() << "recompiling shaders due to settings change"; + for (size_t i = 0; affectedShaders[i]; i++) { + affectedShaders[i]->recompile(defines); + } +} + static void setup_weather(Shader& shader, const Weather& weather) { shader.uniform1f("u_weatherFogOpacity", weather.fogOpacity()); shader.uniform1f("u_weatherFogDencity", weather.fogDencity()); @@ -282,33 +325,6 @@ void WorldRenderer::renderLines( } } -void WorldRenderer::refreshSettings(Shader** shaders) { - const auto& graphics = engine.getSettings().graphics; - gbufferPipeline = graphics.advancedRender.get(); - - int shadowsQuality = graphics.shadowsQuality.get() * gbufferPipeline; - shadowMapping->setQuality(shadowsQuality); - - CompileTimeShaderSettings currentSettings { - gbufferPipeline, - shadowsQuality != 0, - graphics.ssao.get() && gbufferPipeline}; - if (prevCTShaderSettings.advancedRender != currentSettings.advancedRender || - prevCTShaderSettings.shadows != currentSettings.shadows || - prevCTShaderSettings.ssao != currentSettings.ssao) { - std::vector defines; - if (currentSettings.shadows) defines.emplace_back("ENABLE_SHADOWS"); - if (currentSettings.ssao) defines.emplace_back("ENABLE_SSAO"); - if (currentSettings.advancedRender) - defines.emplace_back("ADVANCED_RENDER"); - - for (size_t i = 0; shaders[i]; i++) { - shaders[i]->recompile(defines); - } - prevCTShaderSettings = currentSettings; - } -} - void WorldRenderer::update(const Camera& camera, float delta) { timer += delta; weather.update(delta); @@ -327,28 +343,12 @@ void WorldRenderer::renderFrame( const auto& vp = pctx.getViewport(); camera.setAspectRatio(vp.x / static_cast(vp.y)); - - auto& mainShader = assets.require("main"); - auto& entityShader = assets.require("entity"); - auto& cloudsShader = assets.require("clouds"); - auto& translucentShader = assets.require("translucent"); - auto& deferredShader = - assets.require("deferred_lighting").getShader(); - - const auto& settings = engine.getSettings(); - - Shader* affectedShaders[] { - &mainShader, - &entityShader, - &cloudsShader, - &translucentShader, - &deferredShader, - nullptr}; - - refreshSettings(affectedShaders); + if (dirtyShaders) { + refreshSettings(); + dirtyShaders = false; + } const auto& worldInfo = world->getInfo(); - float clouds = weather.clouds(); clouds = glm::max(worldInfo.fog, clouds); float mie = 1.0f + glm::max(worldInfo.fog, clouds * 0.5f) * 2.0f; @@ -371,6 +371,7 @@ void WorldRenderer::renderFrame( setupWorldShader(shader, shadowCamera, engine.getSettings(), 0.0f); chunksRenderer->drawShadowsPass(shadowCamera, shader, camera); }); + const auto& settings = engine.getSettings(); { DrawContext wctx = pctx.sub(); postProcessing.use(wctx, gbufferPipeline); @@ -388,10 +389,13 @@ void WorldRenderer::renderFrame( } float fogFactor = calcFogFactor(); if (gbufferPipeline) { + auto& deferredShader = + assets.require("deferred_lighting").getShader(); deferredShader.use(); setupWorldShader(deferredShader, camera, settings, fogFactor); postProcessing.renderDeferredShading(pctx, assets, timer, camera); } + auto& entityShader = assets.require("entity"); { DrawContext ctx = pctx.sub(); ctx.setDepthTest(true); @@ -423,6 +427,7 @@ void WorldRenderer::renderFrame( wctx.useTexture(advanced_pipeline::TARGET_COLOR, nullptr); // Translucent blocks { + auto& translucentShader = assets.require("translucent"); auto sctx = wctx.sub(); sctx.setCullFace(true); translucentShader.use(); diff --git a/src/graphics/render/WorldRenderer.hpp b/src/graphics/render/WorldRenderer.hpp index d472b7b75..e813edb0b 100644 --- a/src/graphics/render/WorldRenderer.hpp +++ b/src/graphics/render/WorldRenderer.hpp @@ -6,6 +6,7 @@ #include "presets/WeatherPreset.hpp" #include "world/Weather.hpp" #include "window/Camera.hpp" +#include "util/ObjectsKeeper.hpp" #include #include @@ -37,13 +38,7 @@ class TextsRenderer; class CloudsRenderer; struct EngineSettings; -struct CompileTimeShaderSettings { - bool advancedRender = false; - bool shadows = false; - bool ssao = false; -}; - -class WorldRenderer { +class WorldRenderer final : public util::ObjectsKeeper { public: static bool showChunkBorders; static bool showEntitiesDebug; @@ -90,7 +85,7 @@ private: bool lightsDebug = false; bool gbufferPipeline = false; - CompileTimeShaderSettings prevCTShaderSettings {}; + bool dirtyShaders = false; /// @brief Render block selection lines void renderBlockSelection(); @@ -122,7 +117,7 @@ private: bool hudVisible ); - void refreshSettings(Shader** shaders); + void refreshSettings(); float calcFogFactor() const; public: From 07c35f001cca05ca1699a7887b732ab81566ee41 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 18 Jun 2026 22:14:16 +0300 Subject: [PATCH 029/217] update workflows --- .github/workflows/appimage.yml | 2 +- .github/workflows/macos.yml | 2 +- .github/workflows/windows-clang.yml | 2 +- .github/workflows/windows.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/appimage.yml b/.github/workflows/appimage.yml index f43254bd3..b52b8cd16 100644 --- a/.github/workflows/appimage.yml +++ b/.github/workflows/appimage.yml @@ -4,7 +4,7 @@ on: push: branches: [ "main", "release-**"] pull_request: - branches: [ "main", "dev" ] + branches: [ "main", "dev", "environments" ] workflow_call: inputs: build_name: diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index c3f5156f9..a8966db56 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -4,7 +4,7 @@ on: push: branches: [ "main", "release-**"] pull_request: - branches: [ "main", "dev" ] + branches: [ "main", "dev", "environments" ] workflow_call: inputs: build_name: diff --git a/.github/workflows/windows-clang.yml b/.github/workflows/windows-clang.yml index 5dd91a576..dd5186362 100644 --- a/.github/workflows/windows-clang.yml +++ b/.github/workflows/windows-clang.yml @@ -4,7 +4,7 @@ on: push: branches: [ "main", "release-**"] pull_request: - branches: [ "main", "dev" ] + branches: [ "main", "dev", "environments" ] workflow_call: inputs: build_name: diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index f248b6ab6..208fcd8c3 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -4,7 +4,7 @@ on: push: branches: [ "main", "release-**"] pull_request: - branches: [ "main", "dev" ] + branches: [ "main", "dev", "environments" ] jobs: build-windows: From 22f09d365979685a83f19607325b3ff52448eca8 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 19 Jun 2026 00:01:14 +0300 Subject: [PATCH 030/217] start splitting render pipelines & remove the obsolete world fog parameter --- src/frontend/debug_panel.cpp | 6 - src/graphics/core/Shadows.cpp | 2 +- src/graphics/render/DebugLinesRenderer.cpp | 2 +- src/graphics/render/DebugLinesRenderer.hpp | 2 +- src/graphics/render/WorldRenderer.cpp | 317 +++++++++++++-------- src/graphics/render/WorldRenderer.hpp | 32 ++- src/world/World.cpp | 6 - src/world/World.hpp | 3 - 8 files changed, 223 insertions(+), 147 deletions(-) diff --git a/src/frontend/debug_panel.cpp b/src/frontend/debug_panel.cpp index 780bbf00a..ba0cc2205 100644 --- a/src/frontend/debug_panel.cpp +++ b/src/frontend/debug_panel.cpp @@ -310,12 +310,6 @@ std::shared_ptr create_debug_panel( bar->setConsumer([&](double val) { worldInfo.daytime = val; }); panel->add(bar); } - if (allowDebugCheats) { - auto bar = std::make_shared(gui, 0.0f, 1.0f, 0.0f, 0.005f, 8); - bar->setSupplier([&]() { return worldInfo.fog; }); - bar->setConsumer([&](double val) { worldInfo.fog = val; }); - panel->add(bar); - } { auto checkbox = std::make_shared( gui, L"Show Chunk Borders", glm::vec2(400, 24) diff --git a/src/graphics/core/Shadows.cpp b/src/graphics/core/Shadows.cpp index c9ecc108d..593e4cb4e 100644 --- a/src/graphics/core/Shadows.cpp +++ b/src/graphics/core/Shadows.cpp @@ -100,7 +100,7 @@ void Shadows::setup(Shader& shader, const Weather& weather) { return; } const auto& worldInfo = level.getWorld()->getInfo(); - float cloudsIntensity = glm::max(worldInfo.fog, weather.clouds()); + float cloudsIntensity = weather.clouds(); float shadowsOpacity = 1.0f - cloudsIntensity; shadowsOpacity *= glm::sqrt(glm::abs( glm::mod((worldInfo.daytime + 0.5f) * 2.0f, 1.0f) * 2.0f - 1.0f diff --git a/src/graphics/render/DebugLinesRenderer.cpp b/src/graphics/render/DebugLinesRenderer.cpp index df0b1af78..3750b2cd0 100644 --- a/src/graphics/render/DebugLinesRenderer.cpp +++ b/src/graphics/render/DebugLinesRenderer.cpp @@ -93,7 +93,7 @@ void DebugLinesRenderer::drawCoordSystem( } void DebugLinesRenderer::render( - DrawContext& pctx, + const DrawContext& pctx, const Camera& camera, LineBatch& linesBatch, Shader& linesShader, diff --git a/src/graphics/render/DebugLinesRenderer.hpp b/src/graphics/render/DebugLinesRenderer.hpp index 7a9f0a324..bc6609c5a 100644 --- a/src/graphics/render/DebugLinesRenderer.hpp +++ b/src/graphics/render/DebugLinesRenderer.hpp @@ -20,7 +20,7 @@ public: /// @param linesShader Shader used for rendering lines /// @param showChunkBorders Whether to show chunk borders void render( - DrawContext& ctx, + const DrawContext& ctx, const Camera& camera, LineBatch& linesBatch, Shader& linesShader, diff --git a/src/graphics/render/WorldRenderer.cpp b/src/graphics/render/WorldRenderer.cpp index e59d4392f..353c5fabd 100644 --- a/src/graphics/render/WorldRenderer.cpp +++ b/src/graphics/render/WorldRenderer.cpp @@ -70,10 +70,10 @@ bool WorldRenderer::showEntitiesDebug = false; template static ObserverHandler observe_setting( - ObservableSetting& setting, bool& dirtyShaders + ObservableSetting& setting, bool& dirtySettings ) { - return setting.observe([&dirtyShaders](auto const&) { - dirtyShaders = true; + return setting.observe([&dirtySettings](auto const&) { + dirtySettings = true; }); } @@ -133,9 +133,9 @@ WorldRenderer::WorldRenderer( debugLines = std::make_unique(level); cloudsRenderer = std::make_unique(); - keepAlive(observe_setting(settings.graphics.advancedRender, dirtyShaders)); - keepAlive(observe_setting(settings.graphics.shadowsQuality, dirtyShaders)); - keepAlive(observe_setting(settings.graphics.ssao, dirtyShaders)); + keepAlive(observe_setting(settings.graphics.advancedRender, dirtySettings)); + keepAlive(observe_setting(settings.graphics.shadowsQuality, dirtySettings)); + keepAlive(observe_setting(settings.graphics.ssao, dirtySettings)); } WorldRenderer::~WorldRenderer() = default; @@ -255,12 +255,12 @@ void WorldRenderer::renderOpaque( modelBatch->render(); particles->render(camera); - auto& shader = assets.require("main"); + auto& mainShader = assets.require("main"); auto& cloudsShader = assets.require("clouds"); - setupWorldShader(shader, camera, settings, fogFactor); + setupWorldShader(mainShader, camera, settings, fogFactor); - chunksRenderer->drawChunks(camera, shader); + chunksRenderer->drawChunks(camera, mainShader); blockWraps->draw(ctx); if (level.environment.sky.clouds) { @@ -275,7 +275,7 @@ void WorldRenderer::renderOpaque( if (hudVisible) { auto& linesShader = assets.require("lines"); - renderLines(camera, linesShader, ctx); + renderInWorldLines(camera, linesShader, ctx); } } @@ -308,7 +308,7 @@ void WorldRenderer::renderBlockSelection() { lineBatch->flush(); } -void WorldRenderer::renderLines( +void WorldRenderer::renderInWorldLines( const Camera& camera, Shader& linesShader, const DrawContext& pctx ) { linesShader.use(); @@ -327,159 +327,224 @@ void WorldRenderer::renderLines( void WorldRenderer::update(const Camera& camera, float delta) { timer += delta; + chunksRenderer->update(); weather.update(delta); precipitation->update(delta); particles->update(camera, delta); } +void WorldRenderer::renderFrameClassic( + const DrawContext& pctx, + Camera& camera, + bool hudVisible, + PostProcessing& postProcessing +) { + const auto& worldInfo = level.getWorld()->getInfo(); + + DrawContext ctx = pctx.sub(); + ctx.setDepthTest(true); + ctx.useTexture(advanced_pipeline::TARGET_COLOR, nullptr); + ctx.setFramebuffer(postProcessing.getFramebuffer()); + + skybox->draw( + level.environment, ctx, camera, worldInfo.daytime, weather.clouds() + ); + + if (debug && hudVisible) { + renderDebugLines(ctx, camera); + } + + DrawContext wctx = ctx.sub(); + wctx.useTexture(advanced_pipeline::TARGET_SKYBOX, skybox->getCubemap()); + wctx.useTexture(advanced_pipeline::TARGET_COLOR, nullptr); + // Translucent blocks + { + const auto& settings = engine.getSettings(); + auto& translucentShader = assets.require("translucent"); + auto sctx = wctx.sub(); + sctx.setCullFace(true); + translucentShader.use(); + setupWorldShader(translucentShader, camera, settings, calcFogFactor()); + chunksRenderer->drawSortedMeshes(camera, translucentShader); + } + + renderWeatherEffects(camera); + glBindFramebuffer(GL_FRAMEBUFFER, 0); +} + +void WorldRenderer::renderFrameAdvanced( + const DrawContext& pctx, + Camera& camera, + bool hudVisible, + PostProcessing& postProcessing +) { + const auto& settings = engine.getSettings(); + const auto& worldInfo = level.getWorld()->getInfo(); + float fogFactor = calcFogFactor(); + + shadowMapping->refresh(camera, pctx, [this, &camera](Camera& shadowCamera) { + auto& shader = assets.require("shadows"); + setupWorldShader(shader, shadowCamera, engine.getSettings(), 0.0f); + chunksRenderer->drawShadowsPass(shadowCamera, shader, camera); + }); + + auto& deferredShader = + assets.require("deferred_lighting").getShader(); + deferredShader.use(); + setupWorldShader(deferredShader, camera, settings, fogFactor); + postProcessing.renderDeferredShading(pctx, assets, timer, camera); + + DrawContext ctx = pctx.sub(); + ctx.setDepthTest(true); + ctx.useTexture(advanced_pipeline::TARGET_COLOR, nullptr); + + postProcessing.bindDepthBuffer(); + + // Background sky plane + skybox->draw( + level.environment, ctx, camera, worldInfo.daytime, weather.clouds() + ); + if (debug && hudVisible) { + renderDebugLines(ctx, camera); + } + + DrawContext wctx = ctx.sub(); + wctx.useTexture(advanced_pipeline::TARGET_SKYBOX, skybox->getCubemap()); + wctx.useTexture(advanced_pipeline::TARGET_COLOR, nullptr); + // Translucent blocks + { + const auto& settings = engine.getSettings(); + auto& translucentShader = assets.require("translucent"); + auto sctx = wctx.sub(); + sctx.setCullFace(true); + translucentShader.use(); + setupWorldShader(translucentShader, camera, settings, fogFactor); + chunksRenderer->drawSortedMeshes(camera, translucentShader); + } + + renderWeatherEffects(camera); + glBindFramebuffer(GL_FRAMEBUFFER, 0); +} + void WorldRenderer::renderFrame( const DrawContext& pctx, Camera& camera, bool hudVisible, PostProcessing& postProcessing ) { - auto projView = camera.getProjView(); - auto world = level.getWorld(); - const auto& vp = pctx.getViewport(); camera.setAspectRatio(vp.x / static_cast(vp.y)); - if (dirtyShaders) { + if (dirtySettings) { refreshSettings(); - dirtyShaders = false; + dirtySettings = false; } - const auto& worldInfo = world->getInfo(); + const auto& worldInfo = level.getWorld()->getInfo(); float clouds = weather.clouds(); - clouds = glm::max(worldInfo.fog, clouds); - float mie = 1.0f + glm::max(worldInfo.fog, clouds * 0.5f) * 2.0f; float random = rand() / static_cast(RAND_MAX); skybox->refresh( level.environment, pctx, worldInfo.daytime, - mie, + 1.0f + clouds, weather.skyTint(), weather.highlight * random, 4 ); - - chunksRenderer->update(); - - shadowMapping->refresh(camera, pctx, [this, &camera](Camera& shadowCamera) { - auto& shader = assets.require("shadows"); - setupWorldShader(shader, shadowCamera, engine.getSettings(), 0.0f); - chunksRenderer->drawShadowsPass(shadowCamera, shader, camera); - }); - const auto& settings = engine.getSettings(); - { - DrawContext wctx = pctx.sub(); - postProcessing.use(wctx, gbufferPipeline); - display::clearDepth(); - - /* Main opaque pass (GBuffer pass) */ { - DrawContext ctx = wctx.sub(); - ctx.setDepthTest(true); - ctx.setCullFace(true); - ctx.useTexture(advanced_pipeline::TARGET_SKYBOX, skybox->getCubemap()); - ctx.useTexture(advanced_pipeline::TARGET_COLOR, nullptr); - renderOpaque(ctx, camera, settings, hudVisible); - } - texts->render(pctx, camera, settings, hudVisible, true); - } - float fogFactor = calcFogFactor(); + renderOpaquePass(pctx, camera, hudVisible, postProcessing); if (gbufferPipeline) { - auto& deferredShader = - assets.require("deferred_lighting").getShader(); - deferredShader.use(); - setupWorldShader(deferredShader, camera, settings, fogFactor); - postProcessing.renderDeferredShading(pctx, assets, timer, camera); - } - auto& entityShader = assets.require("entity"); - { - DrawContext ctx = pctx.sub(); - ctx.setDepthTest(true); - ctx.useTexture(advanced_pipeline::TARGET_COLOR, nullptr); - - if (gbufferPipeline) { - postProcessing.bindDepthBuffer(); - } else { - ctx.setFramebuffer(postProcessing.getFramebuffer()); - } - - // Background sky plane - skybox->draw( - level.environment, ctx, camera, worldInfo.daytime, clouds - ); - - auto& linesShader = assets.require("lines"); - linesShader.use(); - if (debug && hudVisible) { - debugLines->render( - ctx, camera, *lineBatch, linesShader, showChunkBorders - ); - } - linesShader.uniformMatrix("u_projview", projView); - lineBatch->flush(); - - DrawContext wctx = ctx.sub(); - wctx.useTexture(advanced_pipeline::TARGET_SKYBOX, skybox->getCubemap()); - wctx.useTexture(advanced_pipeline::TARGET_COLOR, nullptr); - // Translucent blocks - { - auto& translucentShader = assets.require("translucent"); - auto sctx = wctx.sub(); - sctx.setCullFace(true); - translucentShader.use(); - setupWorldShader(translucentShader, camera, settings, fogFactor); - chunksRenderer->drawSortedMeshes(camera, translucentShader); - } - - // Weather effects - entityShader.use(); - setupWorldShader(entityShader, camera, settings, fogFactor); - - std::array weatherInstances { - &weather.a, &weather.b}; - for (const auto& weather : weatherInstances) { - float maxIntensity = weather->fall.maxIntensity; - float zero = weather->fall.minOpacity; - float one = weather->fall.maxOpacity; - float t = (weather->intensity * (one - zero)) * maxIntensity + zero; - entityShader.uniform1i("u_alphaClip", weather->fall.opaque); - entityShader.uniform1f( - "u_opacity", weather->fall.opaque ? t * t : t - ); - if (weather->intensity > 1.e-3f && !weather->fall.texture.empty()) { - precipitation->render(camera, *weather); - } - } - glBindFramebuffer(GL_FRAMEBUFFER, 0); + renderFrameAdvanced(pctx, camera, hudVisible, postProcessing); + } else { + renderFrameClassic(pctx, camera, hudVisible, postProcessing); } postProcessing.render(pctx, assets, timer, camera); if (player.currentCamera == player.fpCamera) { - DrawContext ctx = pctx.sub(); - ctx.setDepthTest(true); - ctx.setCullFace(true); - ctx.useTexture(advanced_pipeline::TARGET_SKYBOX, skybox->getCubemap()); - ctx.useTexture(advanced_pipeline::TARGET_COLOR, nullptr); - display::clearDepth(); - - // prepare modified HUD camera - Camera hudcam = camera; - hudcam.far = 10.0f; - hudcam.setFov(0.9f); - hudcam.position = {}; - - setupWorldShader(entityShader, hudcam, engine.getSettings(), 0.0f); - hands->render(camera); + renderHandsPass(pctx, camera); } renderBlockOverlay(pctx); glActiveTexture(GL_TEXTURE0); } + +void WorldRenderer::renderOpaquePass( + const DrawContext& pctx, + Camera& camera, + bool hudVisible, + PostProcessing& postProcessing +) { + const auto& settings = engine.getSettings(); + + DrawContext wctx = pctx.sub(); + postProcessing.use(wctx, gbufferPipeline); + display::clearDepth(); + + /* Main opaque pass (GBuffer pass) */ { + DrawContext ctx = wctx.sub(); + ctx.setDepthTest(true); + ctx.setCullFace(true); + ctx.useTexture(advanced_pipeline::TARGET_SKYBOX, skybox->getCubemap()); + ctx.useTexture(advanced_pipeline::TARGET_COLOR, nullptr); + renderOpaque(ctx, camera, settings, hudVisible); + } + texts->render(wctx, camera, settings, hudVisible, true); +} + +void WorldRenderer::renderWeatherEffects(Camera& camera) { + const auto& settings = engine.getSettings(); + auto& entityShader = assets.require("entity"); + entityShader.use(); + setupWorldShader(entityShader, camera, settings, calcFogFactor()); + + std::array weatherInstances { + &weather.a, &weather.b}; + for (const auto& weather : weatherInstances) { + float maxIntensity = weather->fall.maxIntensity; + float zero = weather->fall.minOpacity; + float one = weather->fall.maxOpacity; + float t = (weather->intensity * (one - zero)) * maxIntensity + zero; + entityShader.uniform1i("u_alphaClip", weather->fall.opaque); + entityShader.uniform1f( + "u_opacity", weather->fall.opaque ? t * t : t + ); + if (weather->intensity > 1.e-3f && !weather->fall.texture.empty()) { + precipitation->render(camera, *weather); + } + } +} + +void WorldRenderer::renderDebugLines(const DrawContext& ctx, Camera& camera) { + auto& linesShader = assets.require("lines"); + linesShader.use(); + debugLines->render( + ctx, camera, *lineBatch, linesShader, showChunkBorders + ); + linesShader.uniformMatrix("u_projview", camera.getProjView()); + lineBatch->flush(); +} + +void WorldRenderer::renderHandsPass(const DrawContext& pctx, Camera& camera) { + auto& entityShader = assets.require("entity"); + + DrawContext ctx = pctx.sub(); + ctx.setDepthTest(true); + ctx.setCullFace(true); + ctx.useTexture(advanced_pipeline::TARGET_SKYBOX, skybox->getCubemap()); + ctx.useTexture(advanced_pipeline::TARGET_COLOR, nullptr); + display::clearDepth(); + + // prepare modified HUD camera + Camera hudcam = camera; + hudcam.far = 10.0f; + hudcam.setFov(0.9f); + hudcam.position = {}; + + setupWorldShader(entityShader, hudcam, engine.getSettings(), 0.0f); + hands->render(camera); +} + void WorldRenderer::renderBlockOverlay(const DrawContext& wctx) { int x = std::floor(player.currentCamera->position.x); int y = std::floor(player.currentCamera->position.y); diff --git a/src/graphics/render/WorldRenderer.hpp b/src/graphics/render/WorldRenderer.hpp index e813edb0b..d56c21cbf 100644 --- a/src/graphics/render/WorldRenderer.hpp +++ b/src/graphics/render/WorldRenderer.hpp @@ -10,7 +10,6 @@ #include #include -#include #include class Assets; @@ -85,7 +84,7 @@ private: bool lightsDebug = false; bool gbufferPipeline = false; - bool dirtyShaders = false; + bool dirtySettings = false; /// @brief Render block selection lines void renderBlockSelection(); @@ -93,7 +92,7 @@ private: /// @brief Render lines (selection and debug) /// @param camera active camera /// @param linesShader shader used - void renderLines( + void renderInWorldLines( const Camera& camera, Shader& linesShader, const DrawContext& pctx ); @@ -117,6 +116,33 @@ private: bool hudVisible ); + void renderOpaquePass( + const DrawContext& context, + Camera& camera, + bool hudVisible, + PostProcessing& postProcessing + ); + + void renderWeatherEffects(Camera& camera); + + void renderHandsPass(const DrawContext& pctx, Camera& camera); + + void renderDebugLines(const DrawContext& context, Camera& camera); + + void renderFrameClassic( + const DrawContext& context, + Camera& camera, + bool hudVisible, + PostProcessing& postProcessing + ); + + void renderFrameAdvanced( + const DrawContext& context, + Camera& camera, + bool hudVisible, + PostProcessing& postProcessing + ); + void refreshSettings(); float calcFogFactor() const; diff --git a/src/world/World.cpp b/src/world/World.cpp index 2acc7edaa..8bbc78b94 100644 --- a/src/world/World.cpp +++ b/src/world/World.cpp @@ -216,9 +216,6 @@ void WorldInfo::deserialize(const dv::value& root) { daytimeSpeed = timeobj["day-time-speed"].asNumber(); totalTime = timeobj["total-time"].asNumber(); } - if (root.has("weather")) { - fog = root["weather"]["fog"].asNumber(); - } nextInventoryId = root["next-inventory-id"].asInteger(2); nextEntityId = root["next-entity-id"].asInteger(1); root.at("next-player-id").get(nextPlayerId); @@ -244,9 +241,6 @@ dv::value WorldInfo::serialize() const { timeobj["day-time-speed"] = daytimeSpeed; timeobj["total-time"] = totalTime; - root["weather"] = dv::object(); - root["weather"]["fog"] = fog; - root["next-inventory-id"] = nextInventoryId; root["next-entity-id"] = nextEntityId; root["next-player-id"] = nextPlayerId; diff --git a/src/world/World.hpp b/src/world/World.hpp index 14649fb1b..7af7bd1db 100644 --- a/src/world/World.hpp +++ b/src/world/World.hpp @@ -39,9 +39,6 @@ struct WorldInfo : public Serializable { /// @brief total time passed in the world (not depending on daytimeSpeed) double totalTime = 0.0; - /// @brief will be replaced with weather in future - float fog = 0.0f; - entityid_t nextEntityId = 0; int major = 0, minor = -1; From ac00c561ee6796a46dbdb46f2a1fda92726f2a97 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 19 Jun 2026 00:01:45 +0300 Subject: [PATCH 031/217] move weather from WorldRenderer to LevelFrontend --- src/frontend/LevelFrontend.cpp | 4 ++++ src/frontend/LevelFrontend.hpp | 4 ++++ src/frontend/screens/LevelScreen.cpp | 10 +++++----- src/graphics/render/Decorator.cpp | 1 + src/graphics/render/WorldRenderer.cpp | 4 +++- src/graphics/render/WorldRenderer.hpp | 7 +++---- src/logic/scripting/lua/libs/libweather.cpp | 6 ++++++ 7 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/frontend/LevelFrontend.cpp b/src/frontend/LevelFrontend.cpp index d70f09ba2..2143a4778 100644 --- a/src/frontend/LevelFrontend.cpp +++ b/src/frontend/LevelFrontend.cpp @@ -147,3 +147,7 @@ const ContentGfxCache& LevelFrontend::getContentGfxCache() const { LevelController& LevelFrontend::getController() const { return controller; } + +Weather& LevelFrontend::getWeather() { + return weather; +} diff --git a/src/frontend/LevelFrontend.hpp b/src/frontend/LevelFrontend.hpp index 451f88675..28c8a5832 100644 --- a/src/frontend/LevelFrontend.hpp +++ b/src/frontend/LevelFrontend.hpp @@ -1,5 +1,7 @@ #pragma once +#include "world/Weather.hpp" + #include class Assets; @@ -15,6 +17,7 @@ class LevelFrontend { LevelController& controller; Assets& assets; std::unique_ptr contentCache; + Weather weather {}; public: LevelFrontend( Engine& engine, @@ -28,4 +31,5 @@ public: const ContentGfxCache& getContentGfxCache() const; ContentGfxCache& getContentGfxCache(); LevelController& getController() const; + Weather& getWeather(); }; diff --git a/src/frontend/screens/LevelScreen.cpp b/src/frontend/screens/LevelScreen.cpp index 33b637531..613384b7b 100644 --- a/src/frontend/screens/LevelScreen.cpp +++ b/src/frontend/screens/LevelScreen.cpp @@ -81,7 +81,7 @@ LevelScreen::LevelScreen( auto resetChunks = [=](bool) { player->chunks->saveAndClear(); - renderer->clear(); + renderer->resetCache(); }; keepAlive(settings.graphics.backlight.observe(resetChunks)); keepAlive(settings.graphics.softLighting.observe(resetChunks)); @@ -94,7 +94,7 @@ LevelScreen::LevelScreen( })); keepAlive(input.addCallback(BIND_CHUNKS_RELOAD, [=]() { player->chunks->saveAndClear(); - renderer->clear(); + renderer->resetCache(); return false; })); controller->preQuitCallbacks.listen([this]() { @@ -153,7 +153,7 @@ void LevelScreen::loadDecorations() { } auto data = io::read_object(CLIENT_FILE); if (data.has("weather")) { - renderer->getWeather().deserialize(data["weather"]); + frontend->getWeather().deserialize(data["weather"]); } } @@ -161,7 +161,7 @@ void LevelScreen::saveDecorations() { io::create_directory("world:client"); auto data = dv::object(); - data["weather"] = renderer->getWeather().serialize(); + data["weather"] = frontend->getWeather().serialize(); io::write_json(CLIENT_FILE, data, true); } @@ -257,7 +257,7 @@ void LevelScreen::update(float delta) { hud->update(hudVisible); - const Weather& weather = renderer->getWeather(); + const Weather& weather = frontend->getWeather(); const Player& player = playerController->getPlayer(); const Camera& camera = *player.currentCamera; decorator->update(paused ? 0.0f : delta, camera, weather); diff --git a/src/graphics/render/Decorator.cpp b/src/graphics/render/Decorator.cpp index b988898ce..4e2a79518 100644 --- a/src/graphics/render/Decorator.cpp +++ b/src/graphics/render/Decorator.cpp @@ -24,6 +24,7 @@ #include "voxels/Chunks.hpp" #include "window/Camera.hpp" #include "world/Level.hpp" +#include "world/Weather.hpp" #include "WorldRenderer.hpp" namespace fs = std::filesystem; diff --git a/src/graphics/render/WorldRenderer.cpp b/src/graphics/render/WorldRenderer.cpp index 353c5fabd..01168e602 100644 --- a/src/graphics/render/WorldRenderer.cpp +++ b/src/graphics/render/WorldRenderer.cpp @@ -51,6 +51,7 @@ #include "window/Window.hpp" #include "world/Level.hpp" #include "world/LevelEvents.hpp" +#include "world/Weather.hpp" #include "world/World.hpp" #include "debug/Logger.hpp" @@ -84,6 +85,7 @@ WorldRenderer::WorldRenderer( level(frontend.getLevel()), player(player), assets(*engine.getAssets()), + weather(frontend.getWeather()), frustumCulling(std::make_unique()), lineBatch(std::make_unique()), batch3d(std::make_unique(BATCH3D_CAPACITY)), @@ -590,7 +592,7 @@ void WorldRenderer::renderBlockOverlay(const DrawContext& wctx) { batch3d->flush(); } -void WorldRenderer::clear() { +void WorldRenderer::resetCache() { chunksRenderer->clear(); } diff --git a/src/graphics/render/WorldRenderer.hpp b/src/graphics/render/WorldRenderer.hpp index d56c21cbf..49add0351 100644 --- a/src/graphics/render/WorldRenderer.hpp +++ b/src/graphics/render/WorldRenderer.hpp @@ -4,7 +4,6 @@ #include "typedefs.hpp" #include "presets/WeatherPreset.hpp" -#include "world/Weather.hpp" #include "window/Camera.hpp" #include "util/ObjectsKeeper.hpp" @@ -36,6 +35,7 @@ class Skybox; class TextsRenderer; class CloudsRenderer; struct EngineSettings; +struct Weather; class WorldRenderer final : public util::ObjectsKeeper { public: @@ -54,7 +54,7 @@ public: PostProcessing& postProcessing ); - void clear(); + void resetCache(); void setDebug(bool flag); @@ -66,6 +66,7 @@ private: const Level& level; Player& player; const Assets& assets; + Weather& weather; std::unique_ptr frustumCulling; std::unique_ptr lineBatch; std::unique_ptr batch3d; @@ -77,13 +78,11 @@ private: std::unique_ptr debugLines; std::unique_ptr precipitation; std::unique_ptr cloudsRenderer; - Weather weather {}; float timer = 0.0f; bool debug = false; bool lightsDebug = false; bool gbufferPipeline = false; - bool dirtySettings = false; /// @brief Render block selection lines diff --git a/src/logic/scripting/lua/libs/libweather.cpp b/src/logic/scripting/lua/libs/libweather.cpp index 76a94a68b..1ae25156b 100644 --- a/src/logic/scripting/lua/libs/libweather.cpp +++ b/src/logic/scripting/lua/libs/libweather.cpp @@ -2,6 +2,7 @@ #include "world/Level.hpp" #include "world/World.hpp" +#include "world/Weather.hpp" using namespace scripting; @@ -9,6 +10,11 @@ static Weather& require_weather() { if (level == nullptr) { throw std::runtime_error("world is not open"); } + if (renderer == nullptr) { + throw std::runtime_error( + "world frontend is not initialized yet (wait for on_hud_open event)" + ); + } return renderer->getWeather(); } From c73b0360998382cb4ab300a6bf8cc69b782438e8 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sat, 20 Jun 2026 22:17:27 +0300 Subject: [PATCH 032/217] cleanup World references --- src/engine/ServerMainloop.cpp | 2 +- src/frontend/debug_panel.cpp | 4 ++-- src/frontend/screens/LevelScreen.cpp | 6 +++--- src/graphics/core/Shadows.cpp | 6 +++--- src/graphics/render/WorldRenderer.cpp | 10 ++++------ src/items/Inventories.cpp | 4 ++-- src/logic/ChunksController.cpp | 2 +- src/logic/EngineController.cpp | 12 ++++++------ src/logic/EngineController.hpp | 2 +- src/logic/LevelController.cpp | 10 +++++----- src/logic/scripting/lua/libs/libworld.cpp | 6 +++--- src/logic/scripting/scripting.cpp | 2 +- src/objects/Players.cpp | 4 ++-- src/voxels/GlobalChunks.cpp | 4 ++-- src/world/Level.cpp | 8 ++++---- src/world/Level.hpp | 4 ++-- src/world/World.cpp | 2 +- src/world/World.hpp | 2 ++ 18 files changed, 45 insertions(+), 45 deletions(-) diff --git a/src/engine/ServerMainloop.cpp b/src/engine/ServerMainloop.cpp index 54698194e..0c91598b5 100644 --- a/src/engine/ServerMainloop.cpp +++ b/src/engine/ServerMainloop.cpp @@ -57,7 +57,7 @@ void ServerMainloop::run() { } process->update(); if (controller) { - controller->getLevel()->getWorld()->updateTimers(delta); + controller->getLevel()->getWorld().updateTimers(delta); controller->update(glm::min(delta, 0.2), false); } engine.applicationTick(); diff --git a/src/frontend/debug_panel.cpp b/src/frontend/debug_panel.cpp index ba0cc2205..84e85f3e9 100644 --- a/src/frontend/debug_panel.cpp +++ b/src/frontend/debug_panel.cpp @@ -254,7 +254,7 @@ std::shared_ptr create_debug_panel( } })); panel->add(create_label(gui, [&]() { - return L"seed: " + std::to_wstring(level.getWorld()->getSeed()); + return L"seed: " + std::to_wstring(level.getWorld().getSeed()); })); for (int ax = 0; ax < 3; ax++) { @@ -294,7 +294,7 @@ std::shared_ptr create_debug_panel( sub->add(box, glm::vec2(20, 0)); panel->add(sub); } - auto& worldInfo = level.getWorld()->getInfo(); + auto& worldInfo = level.getWorld().getInfo(); panel->add(create_label(gui, [&]() { int hour, minute, second; timeutil::from_value(worldInfo.daytime, hour, minute, second); diff --git a/src/frontend/screens/LevelScreen.cpp b/src/frontend/screens/LevelScreen.cpp index 613384b7b..24ee44378 100644 --- a/src/frontend/screens/LevelScreen.cpp +++ b/src/frontend/screens/LevelScreen.cpp @@ -43,7 +43,7 @@ LevelScreen::LevelScreen( Engine& engine, std::unique_ptr levelPtr, int64_t localPlayer ) : Screen(engine), - world(*levelPtr->getWorld()), + world(levelPtr->getWorld()), postProcessing(std::make_unique( levelPtr->content.getIndices(ResourceType::POST_EFFECT_SLOT).size() )), @@ -98,7 +98,7 @@ LevelScreen::LevelScreen( return false; })); controller->preQuitCallbacks.listen([this]() { - if (!controller->getLevel()->getWorld()->isNameless()) { + if (!controller->getLevel()->getWorld().isNameless()) { saveWorldPreview(); } }); @@ -109,7 +109,7 @@ LevelScreen::LevelScreen( } LevelScreen::~LevelScreen() { - if (!controller->getLevel()->getWorld()->isNameless()) { + if (!controller->getLevel()->getWorld().isNameless()) { saveDecorations(); } scripting::on_frontend_close(); diff --git a/src/graphics/core/Shadows.cpp b/src/graphics/core/Shadows.cpp index 593e4cb4e..8defa40f8 100644 --- a/src/graphics/core/Shadows.cpp +++ b/src/graphics/core/Shadows.cpp @@ -99,7 +99,7 @@ void Shadows::setup(Shader& shader, const Weather& weather) { if (!shadows) { return; } - const auto& worldInfo = level.getWorld()->getInfo(); + const auto& worldInfo = level.getWorld().getInfo(); float cloudsIntensity = weather.clouds(); float shadowsOpacity = 1.0f - cloudsIntensity; shadowsOpacity *= glm::sqrt(glm::abs( @@ -158,8 +158,8 @@ void Shadows::generateShadowsMap( float scale, const std::function& renderShadowPass ) { - auto world = level.getWorld(); - const auto& worldInfo = world->getInfo(); + const auto& world = level.getWorld(); + const auto& worldInfo = world.getInfo(); int resolution = shadowMap.getResolution(); float shadowMapScale = 0.32f / (1 << glm::max(0, quality)) * scale; diff --git a/src/graphics/render/WorldRenderer.cpp b/src/graphics/render/WorldRenderer.cpp index 01168e602..965f3fcbf 100644 --- a/src/graphics/render/WorldRenderer.cpp +++ b/src/graphics/render/WorldRenderer.cpp @@ -201,7 +201,7 @@ void WorldRenderer::setupWorldShader( shader.uniform1f("u_fogCurve", settings.graphics.fogCurve.get()); shader.uniform1i("u_debugLights", lightsDebug); shader.uniform1i("u_debugNormals", false); - shader.uniform1f("u_dayTime", level.getWorld()->getInfo().daytime); + shader.uniform1f("u_dayTime", level.getWorld().getInfo().daytime); shader.uniform2f("u_lightDir", skybox->getLightDir()); shader.uniform1i("u_skybox", advanced_pipeline::TARGET_SKYBOX); @@ -341,7 +341,7 @@ void WorldRenderer::renderFrameClassic( bool hudVisible, PostProcessing& postProcessing ) { - const auto& worldInfo = level.getWorld()->getInfo(); + const auto& worldInfo = level.getWorld().getInfo(); DrawContext ctx = pctx.sub(); ctx.setDepthTest(true); @@ -381,7 +381,7 @@ void WorldRenderer::renderFrameAdvanced( PostProcessing& postProcessing ) { const auto& settings = engine.getSettings(); - const auto& worldInfo = level.getWorld()->getInfo(); + const auto& worldInfo = level.getWorld().getInfo(); float fogFactor = calcFogFactor(); shadowMapping->refresh(camera, pctx, [this, &camera](Camera& shadowCamera) { @@ -401,8 +401,6 @@ void WorldRenderer::renderFrameAdvanced( ctx.useTexture(advanced_pipeline::TARGET_COLOR, nullptr); postProcessing.bindDepthBuffer(); - - // Background sky plane skybox->draw( level.environment, ctx, camera, worldInfo.daytime, weather.clouds() ); @@ -441,7 +439,7 @@ void WorldRenderer::renderFrame( dirtySettings = false; } - const auto& worldInfo = level.getWorld()->getInfo(); + const auto& worldInfo = level.getWorld().getInfo(); float clouds = weather.clouds(); float random = rand() / static_cast(RAND_MAX); diff --git a/src/items/Inventories.cpp b/src/items/Inventories.cpp index 249f5d66d..424bcf2c1 100644 --- a/src/items/Inventories.cpp +++ b/src/items/Inventories.cpp @@ -12,7 +12,7 @@ Inventories::Inventories(Level& level) : level(level) { Inventories::~Inventories() = default; std::shared_ptr Inventories::create(size_t size) { - int64_t id = level.getWorld()->getNextInventoryId(); + int64_t id = level.getWorld().getNextInventoryId(); auto inv = std::make_shared(id, size); store(inv); return inv; @@ -52,7 +52,7 @@ std::shared_ptr Inventories::clone(int64_t id) { auto original = get(id); if (original == nullptr) return nullptr; auto clone = std::make_shared(*original); - clone->setId(level.getWorld()->getNextInventoryId()); + clone->setId(level.getWorld().getNextInventoryId()); store(clone); return clone; } diff --git a/src/logic/ChunksController.cpp b/src/logic/ChunksController.cpp index 1dc791b19..368d641d6 100644 --- a/src/logic/ChunksController.cpp +++ b/src/logic/ChunksController.cpp @@ -27,7 +27,7 @@ ChunksController::ChunksController(Level& level) generator(std::make_unique( level.content.generators.require(level.environment.generator), level.content, - level.getWorld()->getSeed() + level.getWorld().getSeed() )) {} ChunksController::~ChunksController() = default; diff --git a/src/logic/EngineController.cpp b/src/logic/EngineController.cpp index ec9ef0b2a..51b32e0c4 100644 --- a/src/logic/EngineController.cpp +++ b/src/logic/EngineController.cpp @@ -283,7 +283,7 @@ void EngineController::createWorld( // generator-based environments must die in 1.0 bool genBasedEnv = false; - if (auto genDef = generators.find(environment)) { + if (generators.find(environment)) { genBasedEnv = true; } @@ -314,8 +314,8 @@ void EngineController::setLocalPlayer(int64_t player) { localPlayer = player; } -void EngineController::reopenWorld(World* world) { - std::string name = world->wfile->getFolder().name(); +void EngineController::reopenWorld(World& world) { + std::string name = world.wfile->getFolder().name(); engine.onWorldClosed(); openWorld(name, true); } @@ -384,16 +384,16 @@ void EngineController::reconfigPacks( } } else { auto level = controller->getLevel(); - auto world = level->getWorld(); + auto& world = level->getWorld(); controller->processBeforeQuit(); controller->saveWorld(); - auto names = PacksManager::getNames(world->getPacks()); + auto names = PacksManager::getNames(world.getPacks()); auto& manager = contentControl.scan(); reconfig_packs_inside(manager, names, packsToAdd, packsToRemove); const auto& settings = engine.getSettings(); - auto& wfile = *world->wfile; + auto& wfile = *world.wfile; if (!settings.debug.generatorTestMode.get()) { wfile.removeIndices(packsToRemove); } diff --git a/src/logic/EngineController.hpp b/src/logic/EngineController.hpp index baba7ba9e..a40f3476f 100644 --- a/src/logic/EngineController.hpp +++ b/src/logic/EngineController.hpp @@ -41,5 +41,5 @@ public: void setLocalPlayer(int64_t player); - void reopenWorld(World* world); + void reopenWorld(World& world); }; diff --git a/src/logic/LevelController.cpp b/src/logic/LevelController.cpp index 1c40b20df..62a10c219 100644 --- a/src/logic/LevelController.cpp +++ b/src/logic/LevelController.cpp @@ -141,16 +141,16 @@ void LevelController::processBeforeQuit() { } void LevelController::saveWorld() { - auto world = level->getWorld(); - if (world->isNameless()) { + auto& world = level->getWorld(); + if (world.isNameless()) { logger.info() << "nameless world will not be saved"; return; } - logger.info() << "writing world '" << world->getName() << "'"; - world->wfile->createDirectories(); + logger.info() << "writing world '" << world.getName() << "'"; + world.wfile->createDirectories(); scripting::on_world_save(); level->onSave(); - level->getWorld()->write(*level); + level->getWorld().write(*level); } void LevelController::onWorldQuit() { diff --git a/src/logic/scripting/lua/libs/libworld.cpp b/src/logic/scripting/lua/libs/libworld.cpp index 9c50b6c0b..a9105dfe2 100644 --- a/src/logic/scripting/lua/libs/libworld.cpp +++ b/src/logic/scripting/lua/libs/libworld.cpp @@ -33,7 +33,7 @@ static Level& require_level() { } static WorldInfo& require_world_info() { - return require_level().getWorld()->getInfo(); + return require_level().getWorld().getInfo(); } static int l_is_open(lua::State* L) { @@ -136,7 +136,7 @@ static int l_get_chunk_data(lua::State* L) { auto voxelData = std::make_unique(CHUNK_DATA_LEN); std::vector chunkData; if (chunk == nullptr) { - auto& regions = level->getWorld()->wfile->getRegions(); + auto& regions = level->getWorld().wfile->getRegions(); if (!regions.getVoxels(x, z, voxelData.get())) { return 0; } @@ -215,7 +215,7 @@ static int l_save_chunk_data(lua::State* L) { reinterpret_cast(buffer.data()), reinterpret_cast(buffer.data()) + buffer.size() ), - level->getWorld()->wfile->getRegions() + level->getWorld().wfile->getRegions() ); return 0; } diff --git a/src/logic/scripting/scripting.cpp b/src/logic/scripting/scripting.cpp index 37597b058..65caa8877 100644 --- a/src/logic/scripting/scripting.cpp +++ b/src/logic/scripting/scripting.cpp @@ -310,7 +310,7 @@ void scripting::on_world_load(LevelController* controller) { for (auto& pack : content_control->getAllContentPacks()) { lua::emit_event(L, pack.id + ":.worldopen", [](auto L) { return lua::pushboolean( - L, !scripting::level->getWorld()->getInfo().isLoaded + L, !scripting::level->getWorld().getInfo().isLoaded ); }); } diff --git a/src/objects/Players.cpp b/src/objects/Players.cpp index c100e3d67..04c1aa2aa 100644 --- a/src/objects/Players.cpp +++ b/src/objects/Players.cpp @@ -67,7 +67,7 @@ Player* Players::getNearest(const glm::vec3& position) const { } Player* Players::create(int64_t id) { - int64_t& nextPlayerID = level.getWorld()->getInfo().nextPlayerId; + int64_t& nextPlayerID = level.getWorld().getInfo().nextPlayerId; if (id == NONE) { id = nextPlayerID++; } else { @@ -146,7 +146,7 @@ void Players::deserialize(const dv::value& src) { auto& inventory = player->getInventory(); // invalid inventory id pre 0.25 if (inventory->getId() == 0) { - inventory->setId(level.getWorld()->getNextInventoryId()); + inventory->setId(level.getWorld().getNextInventoryId()); } level.inventories->store(inventory); } diff --git a/src/voxels/GlobalChunks.cpp b/src/voxels/GlobalChunks.cpp index d24d8b26d..c081a6a5b 100644 --- a/src/voxels/GlobalChunks.cpp +++ b/src/voxels/GlobalChunks.cpp @@ -107,7 +107,7 @@ std::shared_ptr GlobalChunks::create(int x, int z, bool lighting) { chunks_pool.create(x, z, lighting ? lightmaps_pool.create() : nullptr); chunksMap[keyfrom(x, z)] = chunk; - World& world = *level.getWorld(); + World& world = level.getWorld(); auto& regions = world.wfile.get()->getRegions(); if (regions.getVoxels(chunk->x, chunk->z, voxelDataBuffer.get())) { @@ -197,7 +197,7 @@ void GlobalChunks::save(Chunk* chunk) { if (!entities.empty()) { chunk->flags.entities = true; } - level.getWorld()->wfile->getRegions().put( + level.getWorld().wfile->getRegions().put( chunk, chunk->flags.entities ? json::to_binary(root, true) : std::vector() diff --git a/src/world/Level.cpp b/src/world/Level.cpp index 9e40e90cd..308604ca9 100644 --- a/src/world/Level.cpp +++ b/src/world/Level.cpp @@ -71,12 +71,12 @@ Level::Level( Level::~Level() = default; -World* Level::getWorld() { - return world.get(); +World& Level::getWorld() { + return *world; } -const World* Level::getWorld() const { - return world.get(); +const World& Level::getWorld() const { + return *world; } void Level::onSave() { diff --git a/src/world/Level.hpp b/src/world/Level.hpp index c8945a23c..bf48c35cf 100644 --- a/src/world/Level.hpp +++ b/src/world/Level.hpp @@ -46,9 +46,9 @@ public: ); ~Level(); - World* getWorld(); + World& getWorld(); - const World* getWorld() const; + const World& getWorld() const; void onSave(); diff --git a/src/world/World.cpp b/src/world/World.cpp index 8bbc78b94..d65dd2636 100644 --- a/src/world/World.cpp +++ b/src/world/World.cpp @@ -147,7 +147,7 @@ std::unique_ptr World::load( if (!playerRoot["players"].empty()) { if (!playerRoot["players"][0].has("id")) { - level->getWorld()->getInfo().nextPlayerId++; + level->getWorld().getInfo().nextPlayerId++; } } } diff --git a/src/world/World.hpp b/src/world/World.hpp index 7af7bd1db..dc9439906 100644 --- a/src/world/World.hpp +++ b/src/world/World.hpp @@ -67,6 +67,8 @@ public: const std::vector& packs ); + World(const World&) = delete; + ~World(); /// @brief Update world day-time and total time From 5424f51f7035dfc4da2f77a9c19821da2f9bae5b Mon Sep 17 00:00:00 2001 From: Onran Date: Sun, 21 Jun 2026 19:04:58 +0900 Subject: [PATCH 033/217] added io_stream.wrap_bytearray --- doc/ru/scripting/io_stream.md | 10 ++ .../internal/stream_providers/bytearray.lua | 126 ++++++++++++++++++ res/scripts/stdlib.lua | 2 + 3 files changed, 138 insertions(+) create mode 100644 res/modules/internal/stream_providers/bytearray.lua diff --git a/doc/ru/scripting/io_stream.md b/doc/ru/scripting/io_stream.md index cc3403eda..2362682ad 100644 --- a/doc/ru/scripting/io_stream.md +++ b/doc/ru/scripting/io_stream.md @@ -188,6 +188,16 @@ io_stream:close() -- Вызывает ioLib.flush() в all flush-режиме io_stream:flush() +-- Создаёт новый поток из Bytearray. +-- Может использоваться одновременно как для чтения, так и для записи. +-- Результат записи будет записан в тот же Bytearray, что был передан в функцию. +io_stream.wrap_bytearray( + buffer: Bytearray, + + -- по-умолчанию равен true, поскольку функция из вводных аргументов + -- будет использоваться преимущественно для работы с двоичными данными. + [опционально] binaryMode: boolean = true + ) -> io_stream -- Создаёт новый поток с переданным дескриптором и использующим переданную I/O библиотеку. (Более подробно в core:io_stream.lua) io_stream.new( diff --git a/res/modules/internal/stream_providers/bytearray.lua b/res/modules/internal/stream_providers/bytearray.lua new file mode 100644 index 000000000..6909bdf5c --- /dev/null +++ b/res/modules/internal/stream_providers/bytearray.lua @@ -0,0 +1,126 @@ +local io_stream = require "core:io_stream" + +local lib = { } + +local buffers = { } +local positions = { } + +local nextDescriptor = 0 + +local function openDescriptor(buffer) + nextDescriptor = nextDescriptor + 1 + + buffers[nextDescriptor] = buffer + positions[nextDescriptor] = 1 + + return nextDescriptor +end + +local function requireDescriptor(descriptor) + if not buffers[descriptor] then + error("unknown descriptor") + end +end + +function lib.read(descriptor, length) + requireDescriptor(descriptor) + + local buf = buffers[descriptor] + local bufLength = #buf + local pos = positions[descriptor] + + local toRead = math.min(bufLength - pos + 1, length) + + if toRead <= 0 then + return Bytearray() + end + + local segment = buf:slice(pos, toRead) + + positions[descriptor] = pos + toRead + + return segment +end + +function lib.write(descriptor, data) + requireDescriptor(descriptor) + + local buf = buffers[descriptor] + local pos = positions[descriptor] + + local bufLength = #buf + local dataLength = #data + + local endPos = pos + dataLength - 1 + + -- size ensuring + if endPos > bufLength then + for i = bufLength + 1, endPos do + buf[i] = 0 + end + end + + for i = 1, dataLength do + buf[i + pos - 1] = data[i] + end + + positions[descriptor] = pos + dataLength +end + +function lib.seek(descriptor, mode, offset) + requireDescriptor(descriptor) + + local buf = buffers[descriptor] + local bufLength = #buf + + local base + + if mode == 'b' then + base = 1 + elseif mode == 'c' then + base = positions[descriptor] + elseif mode == 'e' then + base = bufLength + 1 + else error('invalid seek mode') end + + local newPos = base + offset + + if newPos < 1 then + error('failed to seek stream') + end + + positions[descriptor] = newPos +end + +function lib.tell(descriptor) + requireDescriptor(descriptor) + + return positions[descriptor] +end + +function lib.flush(descriptor) + requireDescriptor(descriptor) +end + +function lib.is_alive(descriptor) + return buffers[descriptor] ~= nil +end + +function lib.close(descriptor) + requireDescriptor(descriptor) + + buffers[descriptor] = nil + positions[descriptor] = nil +end + +return function(buffer, binaryMode) + if binaryMode == nil then + binaryMode = true + end + + return io_stream.new( + openDescriptor(buffer), + binaryMode, + lib + ) +end \ No newline at end of file diff --git a/res/scripts/stdlib.lua b/res/scripts/stdlib.lua index 093501d75..17da8ba85 100644 --- a/res/scripts/stdlib.lua +++ b/res/scripts/stdlib.lua @@ -332,6 +332,8 @@ else os.pid = ffi.C.getpid() end +require("core:io_stream").wrap_bytearray = require "core:internal/stream_providers/bytearray" + math.randomseed(time.uptime() * 1536227939) rules = require "core:internal/rules" From fdec2d722d2b81e7bdbdfec2209a9df28d10dde0 Mon Sep 17 00:00:00 2001 From: Onran Date: Sun, 21 Jun 2026 19:20:37 +0900 Subject: [PATCH 034/217] fix of io_stream:read_fully for binary and yield mode --- res/modules/io_stream.lua | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/res/modules/io_stream.lua b/res/modules/io_stream.lua index b608a22a9..5350abb9f 100644 --- a/res/modules/io_stream.lua +++ b/res/modules/io_stream.lua @@ -145,8 +145,12 @@ function io_stream:__update_read_buffer() end end -function io_stream:__read(length) +function io_stream:__read(length, fromReadFully) if self.mode == YIELD_MODE then + if fromReadFully then + return self.ioLib.read(self.descriptor, length) + end + local buffer = Bytearray() while #buffer < length do @@ -203,7 +207,9 @@ function io_stream:read_fully(useTable) if self.binaryMode then local result = useTable and Bytearray() or { } - readFully(result, function() return self:__read(self.maxBufferSize) end) + readFully(result, function() return self:__read(self.maxBufferSize, true) end) + + return result else if useTable then local lines = { } From bf98091a9ce86e0cfbcbf5bffaf853e77fcf936f Mon Sep 17 00:00:00 2001 From: Onran Date: Sun, 21 Jun 2026 20:19:24 +0900 Subject: [PATCH 035/217] added low-level ioLib.available for io_stream's --- doc/ru/scripting/io_stream.md | 18 ++++++------- .../internal/stream_providers/bytearray.lua | 9 +++++++ .../internal/stream_providers/file.lua | 1 + .../stream_providers/named_pipe_unix.lua | 14 ++++++++++ .../stream_providers/named_pipe_windows.lua | 16 +++++++++++ res/modules/io_stream.lua | 7 +++-- src/logic/scripting/descriptors_manager.cpp | 27 +++++++++++++++++++ src/logic/scripting/descriptors_manager.hpp | 2 ++ src/logic/scripting/lua/libs/libfile.cpp | 15 +++++++++++ 9 files changed, 97 insertions(+), 12 deletions(-) diff --git a/doc/ru/scripting/io_stream.md b/doc/ru/scripting/io_stream.md index 2362682ad..0c853131f 100644 --- a/doc/ru/scripting/io_stream.md +++ b/doc/ru/scripting/io_stream.md @@ -154,16 +154,6 @@ io_stream:seek( ## Методы Buffered-режима ```lua ---[[ -Если length определён, то возвращает true, если length байт доступно к чтению. Иначе возвращает false - -Если не определён, то возвращает количество байт, которое можно прочитать - ---]] -io_stream:available( - [опционально] length: number -) -> number | boolean - -- Возвращает максимальный размер буферов io_stream:get_max_buffer_size() -> number @@ -174,6 +164,14 @@ io_stream:set_max_buffer_size(max_size: number) ## Методы контроля состояния потока ```lua +--[[ +Если length определён, то возвращает true, если length байт доступно к чтению. Иначе возвращает false. + +Если не определён, то возвращает количество байт, которое можно прочитать. +--]] +io_stream:available( + [опционально] length: number +) -> number | boolean -- Возвращает true, если поток открыт на данный момент io_stream:is_alive() -> bool diff --git a/res/modules/internal/stream_providers/bytearray.lua b/res/modules/internal/stream_providers/bytearray.lua index 6909bdf5c..35a15b654 100644 --- a/res/modules/internal/stream_providers/bytearray.lua +++ b/res/modules/internal/stream_providers/bytearray.lua @@ -102,6 +102,15 @@ function lib.flush(descriptor) requireDescriptor(descriptor) end +function lib.available(descriptor) + requireDescriptor(descriptor) + + local buf = buffers[descriptor] + local pos = positions[descriptor] + + return math.max(#buf - pos + 1, 0) +end + function lib.is_alive(descriptor) return buffers[descriptor] ~= nil end diff --git a/res/modules/internal/stream_providers/file.lua b/res/modules/internal/stream_providers/file.lua index ec81a42c0..f73961e22 100644 --- a/res/modules/internal/stream_providers/file.lua +++ b/res/modules/internal/stream_providers/file.lua @@ -5,6 +5,7 @@ local lib = { write = file.__write_descriptor, seek = file.__seek_descriptor, flush = file.__flush_descriptor, + available = file.__available_descriptor, is_alive = file.__has_descriptor, close = file.__close_descriptor } diff --git a/res/modules/internal/stream_providers/named_pipe_unix.lua b/res/modules/internal/stream_providers/named_pipe_unix.lua index 8b924f47a..237910ecb 100644 --- a/res/modules/internal/stream_providers/named_pipe_unix.lua +++ b/res/modules/internal/stream_providers/named_pipe_unix.lua @@ -9,6 +9,7 @@ int close(int fd); ssize_t read(int fd, void *buf, size_t count); ssize_t write(int fd, const void *buf, size_t count); int fcntl(int fd, int cmd, ...); +int ioctl(int fd, unsigned long request, ...); const char *strerror(int errnum); ]] @@ -20,6 +21,7 @@ local O_WRONLY = 0x1 local O_RDWR = 0x2 local O_NONBLOCK = 0x800 local F_GETFL = 3 +local FIONREAD = 0x541B local function getError() local err = FFI.errno() @@ -66,6 +68,18 @@ function lib.flush(fd) -- no flush on unix end +function lib.available(fd) + if fd == nil or fd < 0 then return 0 end + + local bytes_ready = FFI.new("int[1]", 0) + + if C.ioctl(fd, FIONREAD, bytes_ready) == -1 then + return 0 + end + + return tonumber(bytes_ready[0]) +end + function lib.is_alive(fd) if fd == nil or fd < 0 then return false end diff --git a/res/modules/internal/stream_providers/named_pipe_windows.lua b/res/modules/internal/stream_providers/named_pipe_windows.lua index 3d4b31af1..3a18f4928 100644 --- a/res/modules/internal/stream_providers/named_pipe_windows.lua +++ b/res/modules/internal/stream_providers/named_pipe_windows.lua @@ -105,6 +105,22 @@ function lib.flush(handle) C.FlushFileBuffers(handle) end +function lib.available(handle) + if handle == nil or handle == INVALID_HANDLE_VALUE then + return 0 + end + + local bytes_available = FFI.new("DWORD[1]") + + local success = C.PeekNamedPipe(handle, nil, 0, nil, bytes_available, nil) + + if success == 0 then + return 0 + end + + return tonumber(bytes_available[0]) +end + function lib.is_alive(handle) if handle == nil or handle == INVALID_HANDLE_VALUE then return false diff --git a/res/modules/io_stream.lua b/res/modules/io_stream.lua index 5350abb9f..3dff50ce4 100644 --- a/res/modules/io_stream.lua +++ b/res/modules/io_stream.lua @@ -42,14 +42,17 @@ local function readFully(result, readFunc) end --[[ - descriptor - descriptor of stream for provided I/O library binaryMode - if enabled, most methods will expect bytes instead of strings ioLib - I/O library. Should include the following functions: read(descriptor: int, length: int) -> Bytearray May return bytearray with a smaller size if bytes have not arrived yet or have run out write(descriptor: int, data: Bytearray) + seek(descriptor: int, mode: string, offset: int) + Mode may be 'b' (relative begin), 'c' (relative current), 'e' (relative end + 1) flush(descriptor: int) + available(descriptor: int) -> int + May return 0 if environment is not support available method is_alive(descriptor: int) -> bool close(descriptor: int) --]] @@ -130,7 +133,7 @@ function io_stream:available(length) else return #self.readBuffer >= length end - end + else return self.ioLib.available(self.descriptor) end end function io_stream:__update_read_buffer() diff --git a/src/logic/scripting/descriptors_manager.cpp b/src/logic/scripting/descriptors_manager.cpp index f7458ccc5..0ff2f018b 100644 --- a/src/logic/scripting/descriptors_manager.cpp +++ b/src/logic/scripting/descriptors_manager.cpp @@ -28,6 +28,33 @@ namespace scripting { } } + int descriptors_manager::available(int descriptor) { + if (!is_readable(descriptor)) + return 0; + + auto* stream = descriptors[descriptor]->in.get(); + + auto current_pos = stream->tellg(); + + if (current_pos == -1) { + return 0; + } + + stream->seekg(0, std::ios::end); + + auto end_pos = stream->tellg(); + + stream->seekg(current_pos, std::ios::beg); + + auto remaining = end_pos - current_pos; + + if (remaining > std::numeric_limits::max()) { + return std::numeric_limits::max(); + } + + return static_cast(remaining); + } + bool descriptors_manager::has_descriptor(int descriptor) { return is_readable(descriptor) || is_writeable(descriptor); } diff --git a/src/logic/scripting/descriptors_manager.hpp b/src/logic/scripting/descriptors_manager.hpp index ccd1b67a3..64373c7a4 100644 --- a/src/logic/scripting/descriptors_manager.hpp +++ b/src/logic/scripting/descriptors_manager.hpp @@ -26,6 +26,8 @@ namespace scripting { static void flush(int descriptor); + static int available(int descriptor); + static bool has_descriptor(int descriptor); static bool is_readable(int descriptor); diff --git a/src/logic/scripting/lua/libs/libfile.cpp b/src/logic/scripting/lua/libs/libfile.cpp index 26248379c..4fd006582 100644 --- a/src/logic/scripting/lua/libs/libfile.cpp +++ b/src/logic/scripting/lua/libs/libfile.cpp @@ -397,6 +397,20 @@ static int l_flush_descriptor(lua::State* L) { return 0; } +static int l_available_descriptor(lua::State* L) { + int descriptor = lua::tointeger(L, 1); + + if (!scripting::descriptors_manager::has_descriptor(descriptor)) { + throw std::runtime_error("unknown descriptor"); + } + + if (!scripting::descriptors_manager::is_readable(descriptor)) { + throw std::runtime_error("descriptor is not readable"); + } + + return lua::pushinteger(L, scripting::descriptors_manager::available(descriptor)); +} + static int l_close_descriptor(lua::State* L) { int descriptor = lua::tointeger(L, 1); @@ -443,6 +457,7 @@ const luaL_Reg filelib[] = { {"__write_descriptor", lua::wrap}, {"__seek_descriptor", lua::wrap}, {"__flush_descriptor", lua::wrap}, + {"__available_descriptor", lua::wrap}, {"__close_descriptor", lua::wrap}, {"__close_all_descriptors", lua::wrap}, {nullptr, nullptr} From dfd67011a40382e0c10edff648e9d82b45eb169a Mon Sep 17 00:00:00 2001 From: Onran Date: Sun, 21 Jun 2026 20:51:49 +0900 Subject: [PATCH 036/217] added socket:as_stream() --- doc/ru/scripting/builtins/libnetwork.md | 5 ++ .../internal/stream_providers/socket.lua | 76 +++++++++++++++++++ res/scripts/classes.lua | 1 + res/scripts/stdlib.lua | 2 + 4 files changed, 84 insertions(+) create mode 100644 res/modules/internal/stream_providers/socket.lua diff --git a/doc/ru/scripting/builtins/libnetwork.md b/doc/ru/scripting/builtins/libnetwork.md index 31f59fb25..5bc5eaafd 100644 --- a/doc/ru/scripting/builtins/libnetwork.md +++ b/doc/ru/scripting/builtins/libnetwork.md @@ -92,6 +92,11 @@ socket:recv_async( [опционально] usetable: boolean=false ) -> nil|table|Bytearray +-- Оборачивает сокет в io_stream (см. ../io_stream.md) +socket:as_stream( + [опционально] binaryMode: boolean=true +) -> io_stream + -- Закрывает соединение socket:close() diff --git a/res/modules/internal/stream_providers/socket.lua b/res/modules/internal/stream_providers/socket.lua new file mode 100644 index 000000000..88684b0ff --- /dev/null +++ b/res/modules/internal/stream_providers/socket.lua @@ -0,0 +1,76 @@ +local io_stream = require "core:io_stream" + +local lib = { } + +local sockets = { } + +local nextDescriptor = 0 + +local function openDescriptor(socket) + nextDescriptor = nextDescriptor + 1 + + sockets[nextDescriptor] = socket + + return nextDescriptor +end + +local function requireDescriptor(descriptor) + if not sockets[descriptor] then + error("unknown descriptor") + end +end + +function lib.read(descriptor, length) + requireDescriptor(descriptor) + + return sockets[descriptor]:recv(length) +end + +function lib.write(descriptor, data) + requireDescriptor(descriptor) + + sockets[descriptor]:send(data) +end + +function lib.seek(descriptor, mode, offset) + error("cannot seek socket") +end + +function lib.tell(descriptor) + error("cannot tell socket") +end + +function lib.flush(descriptor) + requireDescriptor(descriptor) +end + +function lib.available(descriptor) + requireDescriptor(descriptor) + + return sockets[descriptor]:available() +end + +function lib.is_alive(descriptor) + local socket = sockets[descriptor] + + return socket ~= nil and socket:is_alive() +end + +function lib.close(descriptor) + requireDescriptor(descriptor) + + sockets[descriptor]:close() + sockets[descriptor] = nil +end + +return function(socket, binaryMode) + if binaryMode == nil then + binaryMode = true + end + + return io_stream.new( + openDescriptor(socket), + binaryMode, + lib + ) +end \ No newline at end of file diff --git a/res/scripts/classes.lua b/res/scripts/classes.lua index 664710a30..a057d52e6 100644 --- a/res/scripts/classes.lua +++ b/res/scripts/classes.lua @@ -49,6 +49,7 @@ local Socket = {__index={ end return self:recv(length, usetable) end, + as_stream=network.__as_stream, close=function(self) return network.__close(self.id) end, available=function(self) return network.__available(self.id) or 0 end, is_alive=function(self) return network.__is_alive(self.id) end, diff --git a/res/scripts/stdlib.lua b/res/scripts/stdlib.lua index 17da8ba85..6d25ed86c 100644 --- a/res/scripts/stdlib.lua +++ b/res/scripts/stdlib.lua @@ -334,6 +334,8 @@ end require("core:io_stream").wrap_bytearray = require "core:internal/stream_providers/bytearray" +network.__as_stream = require "core:internal/stream_providers/socket" + math.randomseed(time.uptime() * 1536227939) rules = require "core:internal/rules" From f8317e05c413331aab81f1d271477ec9c00b9bc7 Mon Sep 17 00:00:00 2001 From: Onran Date: Sun, 21 Jun 2026 21:01:53 +0900 Subject: [PATCH 037/217] available in io_stream docs moved to I/O section --- doc/ru/scripting/io_stream.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/doc/ru/scripting/io_stream.md b/doc/ru/scripting/io_stream.md index 0c853131f..eab023f67 100644 --- a/doc/ru/scripting/io_stream.md +++ b/doc/ru/scripting/io_stream.md @@ -139,7 +139,16 @@ io_stream:read_fully( ) -> Bytearray | table | table | string --[[ -Устанавливает позицию в потоке +Если length определён, то возвращает true, если length байт доступно к чтению. Иначе возвращает false. + +Если не определён, то возвращает количество байт, которое можно прочитать. +--]] +io_stream:available( + [опционально] length: number +) -> number | boolean + +--[[ +Устанавливает позицию в потоке (всегда в байтах) Режимы: b - Задаёт позицию относительно начало файла c - Задаёт позицию относительно текущей позиции @@ -164,15 +173,6 @@ io_stream:set_max_buffer_size(max_size: number) ## Методы контроля состояния потока ```lua ---[[ -Если length определён, то возвращает true, если length байт доступно к чтению. Иначе возвращает false. - -Если не определён, то возвращает количество байт, которое можно прочитать. ---]] -io_stream:available( - [опционально] length: number -) -> number | boolean - -- Возвращает true, если поток открыт на данный момент io_stream:is_alive() -> bool From cd040cfeb47f91b29c6d138f745b442483f4cf44 Mon Sep 17 00:00:00 2001 From: Onran Date: Sun, 21 Jun 2026 21:13:30 +0900 Subject: [PATCH 038/217] fix of io_stream:available for non-buffered streams when "length" arg defined --- res/modules/io_stream.lua | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/res/modules/io_stream.lua b/res/modules/io_stream.lua index 3dff50ce4..d2f132802 100644 --- a/res/modules/io_stream.lua +++ b/res/modules/io_stream.lua @@ -125,15 +125,21 @@ function io_stream:set_max_buffer_size(maxBufferSize) end function io_stream:available(length) + local available + if self.mode == BUFFERED_MODE then self:__update_read_buffer() - if not length then - return #self.readBuffer - else - return #self.readBuffer >= length - end - else return self.ioLib.available(self.descriptor) end + available = #self.readBuffer + else + available = self.ioLib.available(self.descriptor) + end + + if not length then + return available + else + return available >= length + end end function io_stream:__update_read_buffer() From dd9ba26f7b019a9558ea8b62949444bf6ec7bc23 Mon Sep 17 00:00:00 2001 From: Onran Date: Mon, 22 Jun 2026 00:29:08 +0900 Subject: [PATCH 039/217] conflicts fix --- .../internal/stream_providers/file.lua | 1 + src/logic/scripting/descriptors_manager.cpp | 131 ------------------ src/logic/scripting/descriptors_manager.hpp | 41 ------ src/logic/scripting/io_descriptors.cpp | 27 ++++ src/logic/scripting/io_descriptors.hpp | 1 + src/logic/scripting/lua/libs/libfile.cpp | 6 +- 6 files changed, 32 insertions(+), 175 deletions(-) delete mode 100644 src/logic/scripting/descriptors_manager.cpp delete mode 100644 src/logic/scripting/descriptors_manager.hpp diff --git a/res/modules/internal/stream_providers/file.lua b/res/modules/internal/stream_providers/file.lua index 178b153d3..5e186ae16 100644 --- a/res/modules/internal/stream_providers/file.lua +++ b/res/modules/internal/stream_providers/file.lua @@ -18,6 +18,7 @@ file.__write_descriptor = nil file.__seek_descriptor = nil file.__tell_descriptor = nil file.__flush_descriptor = nil +file.__available_descriptor = nil file.__has_descriptor = nil file.__close_descriptor = nil diff --git a/src/logic/scripting/descriptors_manager.cpp b/src/logic/scripting/descriptors_manager.cpp deleted file mode 100644 index 0ff2f018b..000000000 --- a/src/logic/scripting/descriptors_manager.cpp +++ /dev/null @@ -1,131 +0,0 @@ -#include "logic/scripting/descriptors_manager.hpp" - -#include "debug/Logger.hpp" - -static debug::Logger logger("descriptors-manager"); - -namespace scripting { - - std::vector> descriptors_manager::descriptors; - - std::istream* descriptors_manager::get_input(int descriptor) { - if (!is_readable(descriptor)) - return nullptr; - - return descriptors[descriptor]->in.get(); - } - - std::ostream* descriptors_manager::get_output(int descriptor) { - if (!is_writeable(descriptor)) - return nullptr; - - return descriptors[descriptor]->out.get(); - } - - void descriptors_manager::flush(int descriptor) { - if (is_writeable(descriptor)) { - descriptors[descriptor]->out->flush(); - } - } - - int descriptors_manager::available(int descriptor) { - if (!is_readable(descriptor)) - return 0; - - auto* stream = descriptors[descriptor]->in.get(); - - auto current_pos = stream->tellg(); - - if (current_pos == -1) { - return 0; - } - - stream->seekg(0, std::ios::end); - - auto end_pos = stream->tellg(); - - stream->seekg(current_pos, std::ios::beg); - - auto remaining = end_pos - current_pos; - - if (remaining > std::numeric_limits::max()) { - return std::numeric_limits::max(); - } - - return static_cast(remaining); - } - - bool descriptors_manager::has_descriptor(int descriptor) { - return is_readable(descriptor) || is_writeable(descriptor); - } - - bool descriptors_manager::is_readable(int descriptor) { - return descriptor >= 0 && descriptor < static_cast(descriptors.size()) - && descriptors[descriptor].has_value() - && descriptors[descriptor]->in != nullptr; - } - - bool descriptors_manager::is_writeable(int descriptor) { - return descriptor >= 0 && descriptor < static_cast(descriptors.size()) - && descriptors[descriptor].has_value() - && descriptors[descriptor]->out != nullptr; - } - - void descriptors_manager::close(int descriptor) { - if (descriptor >= 0 && descriptor < static_cast(descriptors.size())) { - if (descriptors[descriptor].has_value()) { - auto& desc = descriptors[descriptor].value(); - - if (desc.out) - desc.out->flush(); - - desc.in.reset(); - desc.out.reset(); - } - - descriptors[descriptor].reset(); - - descriptors[descriptor] = std::nullopt; - } - } - - int descriptors_manager::open_descriptor(const io::path& path, bool write, bool read) { - std::unique_ptr in; - std::unique_ptr out; - - try { - if (read) - in = io::read(path); - - if (write) - out = io::write(path); - } catch (const std::exception& e) { - logger.error() << "failed to open descriptor for " << path.string() - << ": " << e.what(); - - return -1; - } - - for (int i = 0; i < static_cast(descriptors.size()); ++i) { - if (!descriptors[i].has_value()) { - descriptors[i] = StreamDescriptor{ std::move(in), std::move(out) }; - return i; - } - } - - descriptors.emplace_back(StreamDescriptor{ std::move(in), std::move(out) }); - - return static_cast(descriptors.size() - 1); - } - - - void descriptors_manager::close_all_descriptors() { - for (int i = 0; i < static_cast(descriptors.size()); ++i) { - if (descriptors[i].has_value()) { - close(i); - } - } - - descriptors.clear(); - } -} \ No newline at end of file diff --git a/src/logic/scripting/descriptors_manager.hpp b/src/logic/scripting/descriptors_manager.hpp deleted file mode 100644 index 64373c7a4..000000000 --- a/src/logic/scripting/descriptors_manager.hpp +++ /dev/null @@ -1,41 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -#include "io/io.hpp" - -namespace scripting { - - struct StreamDescriptor { - std::unique_ptr in; - std::unique_ptr out; - }; - - class descriptors_manager { - private: - static std::vector> descriptors; - - public: - static std::istream* get_input(int descriptor); - static std::ostream* get_output(int descriptor); - - static void flush(int descriptor); - - static int available(int descriptor); - - static bool has_descriptor(int descriptor); - - static bool is_readable(int descriptor); - static bool is_writeable(int descriptor); - - static void close(int descriptor); - static int open_descriptor(const io::path& path, bool write, bool read); - - static void close_all_descriptors(); - }; -} \ No newline at end of file diff --git a/src/logic/scripting/io_descriptors.cpp b/src/logic/scripting/io_descriptors.cpp index 132eea0eb..ead8566d1 100644 --- a/src/logic/scripting/io_descriptors.cpp +++ b/src/logic/scripting/io_descriptors.cpp @@ -66,6 +66,33 @@ void io_descriptors::flush(int id) { } } +int io_descriptors::available(int id) { + if (!is_readable(id)) + return 0; + + auto* stream = descriptors[id]->in.get(); + + auto current_pos = stream->tellg(); + + if (current_pos == -1) { + return 0; + } + + stream->seekg(0, std::ios::end); + + auto end_pos = stream->tellg(); + + stream->seekg(current_pos, std::ios::beg); + + auto remaining = end_pos - current_pos; + + if (remaining > std::numeric_limits::max()) { + return std::numeric_limits::max(); + } + + return static_cast(remaining); +} + bool io_descriptors::has_descriptor(int id) { return id >= 0 && id < static_cast(::descriptors.size()) && ::descriptors[id].has_value(); diff --git a/src/logic/scripting/io_descriptors.hpp b/src/logic/scripting/io_descriptors.hpp index 7e6207791..56ed21a7a 100644 --- a/src/logic/scripting/io_descriptors.hpp +++ b/src/logic/scripting/io_descriptors.hpp @@ -13,6 +13,7 @@ namespace scripting::io_descriptors { std::ostream& require_output(int id); void flush(int id); + int available(int id); bool has_descriptor(int id); diff --git a/src/logic/scripting/lua/libs/libfile.cpp b/src/logic/scripting/lua/libs/libfile.cpp index e51ce51a0..1bcceb6d3 100644 --- a/src/logic/scripting/lua/libs/libfile.cpp +++ b/src/logic/scripting/lua/libs/libfile.cpp @@ -403,15 +403,15 @@ static int l_flush_descriptor(lua::State* L) { static int l_available_descriptor(lua::State* L) { int descriptor = lua::tointeger(L, 1); - if (!scripting::descriptors_manager::has_descriptor(descriptor)) { + if (!io_descriptors::has_descriptor(descriptor)) { throw std::runtime_error("unknown descriptor"); } - if (!scripting::descriptors_manager::is_readable(descriptor)) { + if (!io_descriptors::is_readable(descriptor)) { throw std::runtime_error("descriptor is not readable"); } - return lua::pushinteger(L, scripting::descriptors_manager::available(descriptor)); + return lua::pushinteger(L, io_descriptors::available(descriptor)); } static int l_close_descriptor(lua::State* L) { From 490db13498fb8f204d3a7d297540a337a77fed04 Mon Sep 17 00:00:00 2001 From: Onran Date: Mon, 22 Jun 2026 00:31:58 +0900 Subject: [PATCH 040/217] added tell stub for named pipe stream's --- res/modules/internal/stream_providers/named_pipe_unix.lua | 4 ++++ res/modules/internal/stream_providers/named_pipe_windows.lua | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/res/modules/internal/stream_providers/named_pipe_unix.lua b/res/modules/internal/stream_providers/named_pipe_unix.lua index 237910ecb..614c9aaf4 100644 --- a/res/modules/internal/stream_providers/named_pipe_unix.lua +++ b/res/modules/internal/stream_providers/named_pipe_unix.lua @@ -64,6 +64,10 @@ function lib.seek(fd, mode, offset) error("cannot seek the named pipe") end +function lib.tell(fd) + error("cannot tell the named pipe") +end + function lib.flush(fd) -- no flush on unix end diff --git a/res/modules/internal/stream_providers/named_pipe_windows.lua b/res/modules/internal/stream_providers/named_pipe_windows.lua index 3a18f4928..d79335f7d 100644 --- a/res/modules/internal/stream_providers/named_pipe_windows.lua +++ b/res/modules/internal/stream_providers/named_pipe_windows.lua @@ -101,6 +101,10 @@ function lib.seek(handle, mode, offset) error("cannot seek the named pipe") end +function lib.tell(handle) + error("cannot tell the named pipe") +end + function lib.flush(handle) C.FlushFileBuffers(handle) end From 21019e6737b14a03f8cf7bf7ab0bdac790dfeda0 Mon Sep 17 00:00:00 2001 From: Onran Date: Mon, 22 Jun 2026 00:57:38 +0900 Subject: [PATCH 041/217] added note about tell to io_stream --- res/modules/io_stream.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/res/modules/io_stream.lua b/res/modules/io_stream.lua index 7e5290c1a..3e91daa4e 100644 --- a/res/modules/io_stream.lua +++ b/res/modules/io_stream.lua @@ -50,6 +50,9 @@ ioLib - I/O library. Should include the following functions: write(descriptor: int, data: Bytearray) seek(descriptor: int, mode: string, offset: int) Mode may be 'b' (relative begin), 'c' (relative current), 'e' (relative end + 1) + May throw error if environment is not support seek + tell(descriptor: int) -> int + May throw error if environment is not support tell flush(descriptor: int) available(descriptor: int) -> int May return 0 if environment is not support available method From 82da7e907d569879d8706f381abccb10371e6c7e Mon Sep 17 00:00:00 2001 From: Onran Date: Mon, 22 Jun 2026 14:47:58 +0900 Subject: [PATCH 042/217] removed require to define available, tell, seek in ioLib --- .../internal/stream_providers/bytearray.lua | 4 --- .../stream_providers/named_pipe_unix.lua | 12 -------- .../stream_providers/named_pipe_windows.lua | 8 ----- .../internal/stream_providers/socket.lua | 12 -------- res/modules/io_stream.lua | 29 +++++++++++++------ 5 files changed, 20 insertions(+), 45 deletions(-) diff --git a/res/modules/internal/stream_providers/bytearray.lua b/res/modules/internal/stream_providers/bytearray.lua index 35a15b654..0f168c851 100644 --- a/res/modules/internal/stream_providers/bytearray.lua +++ b/res/modules/internal/stream_providers/bytearray.lua @@ -98,10 +98,6 @@ function lib.tell(descriptor) return positions[descriptor] end -function lib.flush(descriptor) - requireDescriptor(descriptor) -end - function lib.available(descriptor) requireDescriptor(descriptor) diff --git a/res/modules/internal/stream_providers/named_pipe_unix.lua b/res/modules/internal/stream_providers/named_pipe_unix.lua index 614c9aaf4..933f6292b 100644 --- a/res/modules/internal/stream_providers/named_pipe_unix.lua +++ b/res/modules/internal/stream_providers/named_pipe_unix.lua @@ -60,18 +60,6 @@ function lib.write(fd, bytearray) end end -function lib.seek(fd, mode, offset) - error("cannot seek the named pipe") -end - -function lib.tell(fd) - error("cannot tell the named pipe") -end - -function lib.flush(fd) - -- no flush on unix -end - function lib.available(fd) if fd == nil or fd < 0 then return 0 end diff --git a/res/modules/internal/stream_providers/named_pipe_windows.lua b/res/modules/internal/stream_providers/named_pipe_windows.lua index d79335f7d..817925b08 100644 --- a/res/modules/internal/stream_providers/named_pipe_windows.lua +++ b/res/modules/internal/stream_providers/named_pipe_windows.lua @@ -97,14 +97,6 @@ function lib.write(handle, bytearray) end end -function lib.seek(handle, mode, offset) - error("cannot seek the named pipe") -end - -function lib.tell(handle) - error("cannot tell the named pipe") -end - function lib.flush(handle) C.FlushFileBuffers(handle) end diff --git a/res/modules/internal/stream_providers/socket.lua b/res/modules/internal/stream_providers/socket.lua index 88684b0ff..0faedd695 100644 --- a/res/modules/internal/stream_providers/socket.lua +++ b/res/modules/internal/stream_providers/socket.lua @@ -32,18 +32,6 @@ function lib.write(descriptor, data) sockets[descriptor]:send(data) end -function lib.seek(descriptor, mode, offset) - error("cannot seek socket") -end - -function lib.tell(descriptor) - error("cannot tell socket") -end - -function lib.flush(descriptor) - requireDescriptor(descriptor) -end - function lib.available(descriptor) requireDescriptor(descriptor) diff --git a/res/modules/io_stream.lua b/res/modules/io_stream.lua index 3e91daa4e..ef1150237 100644 --- a/res/modules/io_stream.lua +++ b/res/modules/io_stream.lua @@ -47,15 +47,14 @@ binaryMode - if enabled, most methods will expect bytes instead of strings ioLib - I/O library. Should include the following functions: read(descriptor: int, length: int) -> Bytearray May return bytearray with a smaller size if bytes have not arrived yet or have run out + May throw error if descriptor isn't readable write(descriptor: int, data: Bytearray) - seek(descriptor: int, mode: string, offset: int) + May throw error if descriptor isn't writeable + [optional] seek(descriptor: int, mode: string, offset: int) Mode may be 'b' (relative begin), 'c' (relative current), 'e' (relative end + 1) - May throw error if environment is not support seek - tell(descriptor: int) -> int - May throw error if environment is not support tell - flush(descriptor: int) - available(descriptor: int) -> int - May return 0 if environment is not support available method + [optional] tell(descriptor: int) -> int + [optional] flush(descriptor: int) + [optional] available(descriptor: int) -> int is_alive(descriptor: int) -> bool close(descriptor: int) --]] @@ -135,7 +134,7 @@ function io_stream:available(length) available = #self.readBuffer else - available = self.ioLib.available(self.descriptor) + available = self.ioLib.available and self.ioLib.available(self.descriptor) or 0 end if not length then @@ -388,10 +387,18 @@ function io_stream:write(arg, ...) end function io_stream:seek(mode, offset) + if not self.ioLib.seek then + error("cannot seek this stream") + end + self.ioLib.seek(self.descriptor, mode, offset) end function io_stream:tell() + if not self.ioLib.tell then + error("cannot tell this stream") + end + return self.ioLib.tell(self.descriptor) end @@ -418,7 +425,11 @@ function io_stream:flush() self.writeBuffer:clear() end - if self.flushMode ~= FLUSH_MODE_ONLY_BUFFER then self.ioLib.flush(self.descriptor) end + if self.flushMode ~= FLUSH_MODE_ONLY_BUFFER then + if self.ioLib.flush then + self.ioLib.flush(self.descriptor) + elseif self:is_closed() then error("stream is closed") end + end end return io_stream From 15f17a0bc33598812e00fe073fbafa21bdbdf59a Mon Sep 17 00:00:00 2001 From: Onran Date: Mon, 22 Jun 2026 14:49:43 +0900 Subject: [PATCH 043/217] all numbers changed to ints in io_stream docs and documented tell --- doc/ru/scripting/io_stream.md | 51 ++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/doc/ru/scripting/io_stream.md b/doc/ru/scripting/io_stream.md index eab023f67..57f447643 100644 --- a/doc/ru/scripting/io_stream.md +++ b/doc/ru/scripting/io_stream.md @@ -48,13 +48,13 @@ **Независимый флаг** (включается через `set_binary_mode(true)` или при создании потока). Определяет, в каком виде методы `read` и `write` принимают и возвращают данные: -| binary = true | binary = false (по умолчанию) | -|-----------------------------------------|----------------------------------------| +| binary = true | binary = false (по умолчанию) | +|------------------------------------------------------------|----------------------------------------| | Данные — это **байты** (`Bytearray`, таблица чисел 0..255) | Данные — это **текстовые строки** | -| `read(n)` → `Bytearray` или `table`| `read()` → одна строка | -| `read("i4 f")` → распаковка через `byteutil.unpack` | `read(n)` → n строк в таблице | -| `write(Bytearray)` → запись байтов | `write("hello")` → строка + `\n` | -| `write("i4", 42)` → `byteutil.pack` | `write({"a","b"})` → две строки с `\n`| +| `read(n)` → `Bytearray` или `table` | `read()` → одна строка | +| `read("i4 f")` → распаковка через `byteutil.unpack` | `read(n)` → n строк в таблице | +| `write(Bytearray)` → запись байтов | `write("hello")` → строка + `\n` | +| `write("i4", 42)` → `byteutil.pack` | `write({"a","b"})` → две строки с `\n`| `read_line` / `write_line` — работают как в текстовом режиме @@ -90,25 +90,25 @@ io_stream:set_flush_mode(string) --[[ Читает данные из потока В двоичном режиме: - Если arg - number, то читает из потока arg байт и возвращает ввиде Bytearray или таблицы, если useTable = true + Если arg - int, то читает из потока arg байт и возвращает ввиде Bytearray или таблицы, если useTable = true Если arg - string, то функция интерпретирует arg как шаблон для byteutil. Прочитает кол-во байт, которое определено шаблоном, передаст их в byteutil.unpack и вернёт результат В текстовом режиме: - Если arg - number, то читает нужное кол-во строк с окончанием CRLF/LF из arg и возвращает ввиде таблицы. Также, если trimEmptyLines = true, то удаляет пустые строки с начала и конца из итоговой таблицы + Если arg - int, то читает нужное кол-во строк с окончанием CRLF/LF из arg и возвращает ввиде таблицы. Также, если trimEmptyLines = true, то удаляет пустые строки с начала и конца из итоговой таблицы Если arg не определён, то читает одну строку с окончанием CRLF/LF и возвращает её. --]] io_stream:read( - [опционально] arg: number | string, + [опционально] arg: int | string, [опционально] useTable | trimEmptyLines: boolean -) -> Bytearray | table | string | table | ... +) -> Bytearray | table | string | table | ... --[[ Записывает данные в поток В двоичном режиме: Если arg - string, то функция интерпретирует arg как шаблон для byteutil, передаст его и ... в byteutil.pack и результат запишет в поток - Если arg - Bytearray | table, то записывает байты в поток + Если arg - Bytearray | table, то записывает байты в поток В текстовом режиме: Если arg - string, то записывает строку в поток (вместе с окончанием LF) @@ -116,7 +116,7 @@ io_stream:read( Если arg - table, то записывает каждую строку из таблицы отдельно --]] io_stream:write( - arg: Bytearray | table | string | table, + arg: Bytearray | table | string | table, [опционально] ... ) @@ -128,7 +128,7 @@ io_stream:write_line(string) --[[ В двоичном режиме: - Читает все доступные байты из потока и возвращает ввиде Bytearray или table, если useTable = true + Читает все доступные байты из потока и возвращает ввиде Bytearray или table, если useTable = true В текстовом режиме: Читает все доступные строки из потока в table если useTable = true, или в одну строку вместе с окончаниями, если нет @@ -136,16 +136,18 @@ io_stream:write_line(string) --]] io_stream:read_fully( [опционально] useTable: boolean -) -> Bytearray | table | table | string +) -> Bytearray | table | table | string --[[ Если length определён, то возвращает true, если length байт доступно к чтению. Иначе возвращает false. Если не определён, то возвращает количество байт, которое можно прочитать. + +В не буферизированном режиме потока может всегда возвращать 0 или false, если поток не поддерживает available. --]] io_stream:available( - [опционально] length: number -) -> number | boolean + [опционально] length: int +) -> int | boolean --[[ Устанавливает позицию в потоке (всегда в байтах) @@ -153,21 +155,27 @@ io_stream:available( b - Задаёт позицию относительно начало файла c - Задаёт позицию относительно текущей позиции e - Задаёт позицию относительно конца файла + +Может бросать ошибку, если поток не поддерживает seek. --]] io_stream:seek( mode: string - offset: number + offset: int ) + +-- Возвращает текущую позицию в потоке от начала. +-- Может бросать ошибку, если поток не поддерживает tell. +io_stream:tell() -> int ``` ## Методы Buffered-режима ```lua -- Возвращает максимальный размер буферов -io_stream:get_max_buffer_size() -> number +io_stream:get_max_buffer_size() -> int -- Задаёт новый максимальный размер буферов -io_stream:set_max_buffer_size(max_size: number) +io_stream:set_max_buffer_size(max_size: int) ``` ## Методы контроля состояния потока @@ -183,7 +191,8 @@ io_stream:is_closed() -> bool io_stream:close() -- Записывает все данные из write-буфера в поток в buffer/all flush-режимах --- Вызывает ioLib.flush() в all flush-режиме +-- Вызывает ioLib.flush() в all flush-режиме, или ничего не делает, если +-- ioLib не поддерживает flush. io_stream:flush() -- Создаёт новый поток из Bytearray. @@ -200,7 +209,7 @@ io_stream.wrap_bytearray( -- Создаёт новый поток с переданным дескриптором и использующим переданную I/O библиотеку. (Более подробно в core:io_stream.lua) io_stream.new( descriptor: int, - binaryMode: bool, + binaryMode: boolean, ioLib: table, [опционально] mode: string = "default", [опционально] flushMode: string = "all" From 658af207abc31190233e10ab943d8ed17c23e20a Mon Sep 17 00:00:00 2001 From: Onran Date: Mon, 22 Jun 2026 15:09:20 +0900 Subject: [PATCH 044/217] minor change in binary read_fully --- res/modules/io_stream.lua | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/res/modules/io_stream.lua b/res/modules/io_stream.lua index ef1150237..7a760fa69 100644 --- a/res/modules/io_stream.lua +++ b/res/modules/io_stream.lua @@ -218,7 +218,13 @@ function io_stream:read_fully(useTable) if self.binaryMode then local result = useTable and Bytearray() or { } - readFully(result, function() return self:__read(self.maxBufferSize, true) end) + local avail = self:available() + + if avail == 0 then + avail = self.maxBufferSize + end + + readFully(result, function() return self:__read(avail, true) end) return result else From e7bc0bdbda15a31ac1f6fb3efefecc4c4f8a7049 Mon Sep 17 00:00:00 2001 From: Onran Date: Mon, 22 Jun 2026 15:59:45 +0900 Subject: [PATCH 045/217] major fixes of buffered mode in io_stream - fixed underflow and overflow errors: now stream automatically does flush or read --- res/modules/io_stream.lua | 61 +++++++++++++++++---------------------- 1 file changed, 26 insertions(+), 35 deletions(-) diff --git a/res/modules/io_stream.lua b/res/modules/io_stream.lua index 7a760fa69..10f45d6a4 100644 --- a/res/modules/io_stream.lua +++ b/res/modules/io_stream.lua @@ -127,14 +127,10 @@ function io_stream:set_max_buffer_size(maxBufferSize) end function io_stream:available(length) - local available + local available = self.ioLib.available and self.ioLib.available(self.descriptor) or 0 if self.mode == BUFFERED_MODE then - self:__update_read_buffer() - - available = #self.readBuffer - else - available = self.ioLib.available and self.ioLib.available(self.descriptor) or 0 + available = available + #self.readBuffer end if not length then @@ -144,18 +140,6 @@ function io_stream:available(length) end end -function io_stream:__update_read_buffer() - local readed = Bytearray() - - readFully(readed, function(length) return self.ioLib.read(self.descriptor, length) end) - - self.readBuffer:append(readed) - - if #self.readBuffer > self.maxBufferSize then - error "buffer overflow" - end -end - function io_stream:__read(length, fromReadFully) if self.mode == YIELD_MODE then if fromReadFully then @@ -172,27 +156,23 @@ function io_stream:__read(length, fromReadFully) return buffer elseif self.mode == BUFFERED_MODE then - self:__update_read_buffer() + local bufLen = #self.readBuffer - if #self.readBuffer < length then - error "buffer underflow" + if bufLen < length then + self.readBuffer:append( + self.ioLib.read(self.descriptor, self.maxBufferSize - bufLen) + ) end - local copy + bufLen = #self.readBuffer - if #self.readBuffer == length then - copy = Bytearray() - - copy:append(self.readBuffer) + length = math.min(bufLen, length) + local copy = self.readBuffer:slice(1, length) + + if bufLen == length then self.readBuffer:clear() else - copy = Bytearray() - - for i = 1, length do - copy[i] = self.readBuffer[i] - end - self.readBuffer:remove(1, length) end @@ -204,11 +184,22 @@ end function io_stream:__write(data) if self.mode == BUFFERED_MODE then - self.writeBuffer:append(data) + local dataLength = #data - if #self.writeBuffer > self.maxBufferSize then - error "buffer overflow" + if #self.writeBuffer + dataLength > self.maxBufferSize then + self:flush() end + + if dataLength > self.maxBufferSize then + local toWrite = math.floor(dataLength / self.maxBufferSize) * self.maxBufferSize + local toSave = dataLength - toWrite + + self.ioLib.write(self.descriptor, data:slice(1, toWrite)) + + self:flush() + + self.writeBuffer = data:slice(toWrite + 1, toSave) + else self.writeBuffer:append(data) end elseif self.mode == DEFAULT_MODE or self.mode == YIELD_MODE then return self.ioLib.write(self.descriptor, data) end From 36509fb7bca43d53a1946f51991b094cdb716190 Mon Sep 17 00:00:00 2001 From: Onran Date: Mon, 22 Jun 2026 16:03:52 +0900 Subject: [PATCH 046/217] added bytes reserve when creating buffers for stream --- res/modules/io_stream.lua | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/res/modules/io_stream.lua b/res/modules/io_stream.lua index 10f45d6a4..98a37529c 100644 --- a/res/modules/io_stream.lua +++ b/res/modules/io_stream.lua @@ -93,14 +93,19 @@ function io_stream:set_mode(mode) error("invalid stream mode: "..mode) end - if self.mode == BUFFERED_MODE then + if self.writeBuffer then self.writeBuffer:clear() - self.readBuffer:clear() + self.writeBuffer = nil end - if mode == BUFFERED_MODE and not self.writeBuffer then - self.writeBuffer = Bytearray() - self.readBuffer = Bytearray() + if self.readBuffer then + self.readBuffer:clear() + self.readBuffer = nil + end + + if mode == BUFFERED_MODE then + self.writeBuffer = Bytearray(self.maxBufferSize) + self.readBuffer = Bytearray(self.maxBufferSize) end self.mode = mode @@ -124,6 +129,9 @@ end function io_stream:set_max_buffer_size(maxBufferSize) self.maxBufferSize = maxBufferSize + + self.writeBuffer = Bytearray(self.maxBufferSize) + self.readBuffer = Bytearray(self.maxBufferSize) end function io_stream:available(length) From 30ab6c2f5e147869cc91741b9a12d7f3006d166d Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 6 Jul 2026 01:05:44 +0300 Subject: [PATCH 047/217] remove unnecessary drawGroup-based loops --- src/graphics/render/BlocksRenderer.cpp | 336 ++++++++++++------------- src/graphics/render/BlocksRenderer.hpp | 12 +- src/graphics/render/ChunksRenderer.cpp | 7 +- src/voxels/VoxelsVolume.hpp | 20 +- 4 files changed, 180 insertions(+), 195 deletions(-) diff --git a/src/graphics/render/BlocksRenderer.cpp b/src/graphics/render/BlocksRenderer.cpp index 0aae42010..6ccc5dcf6 100644 --- a/src/graphics/render/BlocksRenderer.cpp +++ b/src/graphics/render/BlocksRenderer.cpp @@ -10,15 +10,14 @@ #include "frontend/ContentGfxCache.hpp" const glm::vec3 BlocksRenderer::SUN_VECTOR(0.528265, 0.833149, -0.163704); -const float DIRECTIONAL_LIGHT_FACTOR = 0.3f; +constexpr float DIRECTIONAL_LIGHT_FACTOR = 0.3f; BlocksRenderer::BlocksRenderer( size_t capacity, - const Content& content, + const Block* const* blockDefs, const ContentGfxCache& cache, const EngineSettings& settings -) : content(content), - vertexBuffer(std::make_unique(capacity)), +) : vertexBuffer(std::make_unique(capacity)), indexBuffer(std::make_unique(capacity)), denseIndexBuffer(std::make_unique(capacity)), vertexCount(0), @@ -28,7 +27,7 @@ BlocksRenderer::BlocksRenderer( cache(cache), settings(settings) { - blockDefsCache = content.getIndices()->blocks.getDefs(); + blockDefsCache = blockDefs; } BlocksRenderer::~BlocksRenderer() = default; @@ -482,84 +481,78 @@ glm::vec4 BlocksRenderer::pickSoftLight( } void BlocksRenderer::render( - const voxel* voxels, const int beginEnds[256][2] + const voxel* voxels, int totalBegin, int totalEnd ) { bool denseRender = this->denseRender; bool densePass = this->densePass; bool enableAO = settings.graphics.softLighting.get(); - for (const auto drawGroup : *content.drawGroups) { - int begin = beginEnds[drawGroup][0]; - if (begin == 0) { + + for (int i = totalBegin; i < totalEnd; i++) { + const voxel& vox = voxels[i]; + blockid_t id = vox.id; + blockstate state = vox.state; + const auto& def = *blockDefsCache[id]; + uint8_t variantId = def.getVariantIndex(state.userbits); + const auto& variant = def.getVariant(variantId); + if (id == 0 || state.segment) { continue; } - int end = beginEnds[drawGroup][1]; - for (int i = begin-1; i <= end; i++) { - const voxel& vox = voxels[i]; - blockid_t id = vox.id; - blockstate state = vox.state; - const auto& def = *blockDefsCache[id]; - uint8_t variantId = def.getVariantIndex(state.userbits); - const auto& variant = def.getVariant(variantId); - if (id == 0 || variant.drawGroup != drawGroup || state.segment) { - continue; + if (denseRender != (variant.culling == CullingMode::OPTIONAL)) { + continue; + } + if (def.translucent) { + continue; + } + const UVRegion texfaces[6] { + cache.getRegion(id, variantId, 0, densePass), + cache.getRegion(id, variantId, 1, densePass), + cache.getRegion(id, variantId, 2, densePass), + cache.getRegion(id, variantId, 3, densePass), + cache.getRegion(id, variantId, 4, densePass), + cache.getRegion(id, variantId, 5, densePass) + }; + int x = i % CHUNK_W; + int y = i / (CHUNK_D * CHUNK_W); + int z = (i / CHUNK_D) % CHUNK_W; + switch (def.getModel(state.userbits).type) { + case BlockModelType::BLOCK: + blockCube({x, y, z}, texfaces, def, vox.state, !def.shadeless, + def.ambientOcclusion && enableAO); + break; + case BlockModelType::XSPRITE: { + if (!denseRender) + blockXSprite(x, y, z, glm::vec3(1.0f), + texfaces[FACE_MX], texfaces[FACE_MZ], 1.0f); + break; } - if (denseRender != (variant.culling == CullingMode::OPTIONAL)) { - continue; + case BlockModelType::AABB: { + if (!denseRender) + blockAABB({x, y, z}, texfaces, &def, vox.state.rotation, + !def.shadeless, def.ambientOcclusion && enableAO); + break; } - if (def.translucent) { - continue; - } - const UVRegion texfaces[6] { - cache.getRegion(id, variantId, 0, densePass), - cache.getRegion(id, variantId, 1, densePass), - cache.getRegion(id, variantId, 2, densePass), - cache.getRegion(id, variantId, 3, densePass), - cache.getRegion(id, variantId, 4, densePass), - cache.getRegion(id, variantId, 5, densePass) - }; - int x = i % CHUNK_W; - int y = i / (CHUNK_D * CHUNK_W); - int z = (i / CHUNK_D) % CHUNK_W; - switch (def.getModel(state.userbits).type) { - case BlockModelType::BLOCK: - blockCube({x, y, z}, texfaces, def, vox.state, !def.shadeless, - def.ambientOcclusion && enableAO); - break; - case BlockModelType::XSPRITE: { - if (!denseRender) - blockXSprite(x, y, z, glm::vec3(1.0f), - texfaces[FACE_MX], texfaces[FACE_MZ], 1.0f); - break; - } - case BlockModelType::AABB: { - if (!denseRender) - blockAABB({x, y, z}, texfaces, &def, vox.state.rotation, - !def.shadeless, def.ambientOcclusion && enableAO); - break; - } - case BlockModelType::CUSTOM: { - if (!denseRender) - blockCustomModel( - {x, y, z}, - def, - vox.state, - !def.shadeless, - def.ambientOcclusion && enableAO - ); - break; - } - default: - break; - } - if (overflow) { - return; + case BlockModelType::CUSTOM: { + if (!denseRender) + blockCustomModel( + {x, y, z}, + def, + vox.state, + !def.shadeless, + def.ambientOcclusion && enableAO + ); + break; } + default: + break; + } + if (overflow) { + return; } } } SortingMeshData BlocksRenderer::renderTranslucent( - const voxel* voxels, int beginEnds[256][2] + const voxel* voxels, int totalBegin, int totalEnd ) { SortingMeshData sortingMesh {{}}; @@ -569,106 +562,98 @@ SortingMeshData BlocksRenderer::renderTranslucent( bool densePass = this->densePass; bool enableAO = settings.graphics.softLighting.get(); - for (const auto drawGroup : *content.drawGroups) { - int begin = beginEnds[drawGroup][0]; - if (begin == 0) { + for (int i = totalBegin; i < totalEnd; i++) { + const voxel& vox = voxels[i]; + blockid_t id = vox.id; + blockstate state = vox.state; + const auto& def = *blockDefsCache[id]; + uint8_t variantId = def.getVariantIndex(state.userbits); + if (id == 0 || state.segment) { continue; } - int end = beginEnds[drawGroup][1]; - for (int i = begin-1; i <= end; i++) { - const voxel& vox = voxels[i]; - blockid_t id = vox.id; - blockstate state = vox.state; - const auto& def = *blockDefsCache[id]; - uint8_t variantId = def.getVariantIndex(state.userbits); - const auto& variant = def.getVariant(variantId); - if (id == 0 || variant.drawGroup != drawGroup || state.segment) { - continue; - } - if (!def.translucent) { - continue; - } - const UVRegion texfaces[6] { - cache.getRegion(id, variantId, 0, densePass), - cache.getRegion(id, variantId, 1, densePass), - cache.getRegion(id, variantId, 2, densePass), - cache.getRegion(id, variantId, 3, densePass), - cache.getRegion(id, variantId, 4, densePass), - cache.getRegion(id, variantId, 5, densePass) - }; - int x = i % CHUNK_W; - int y = i / (CHUNK_D * CHUNK_W); - int z = (i / CHUNK_D) % CHUNK_W; - switch (def.getModel(state.userbits).type) { - case BlockModelType::BLOCK: - blockCube({x, y, z}, texfaces, def, vox.state, !def.shadeless, - def.ambientOcclusion && enableAO); - break; - case BlockModelType::XSPRITE: { - blockXSprite(x, y, z, glm::vec3(1.0f), - texfaces[FACE_MX], texfaces[FACE_MZ], 1.0f); - break; - } - case BlockModelType::AABB: { - blockAABB( - {x, y, z}, - texfaces, - &def, - vox.state.rotation, - !def.shadeless, - def.ambientOcclusion && enableAO - ); - break; - } - case BlockModelType::CUSTOM: { - blockCustomModel( - {x, y, z}, - def, - vox.state, - !def.shadeless, - def.ambientOcclusion && enableAO - ); - break; - } - default: - break; - } - if (vertexCount == 0) { - continue; - } - SortingMeshEntry entry { - glm::vec3( - x + chunk->x * CHUNK_W + 0.5f, - y + 0.5f, - z + chunk->z * CHUNK_D + 0.5f - ), - util::Buffer(indexCount), 0}; - - totalSize += entry.vertexData.size(); - - for (int j = 0; j < indexCount; j++) { - std::memcpy( - entry.vertexData.data() + j, - vertexBuffer.get() + indexBuffer[j], - sizeof(ChunkVertex) - ); - ChunkVertex& vertex = entry.vertexData[j]; - - if (!aabbInit) { - aabbInit = true; - aabb.a = aabb.b = vertex.position; - } else { - aabb.addPoint(vertex.position); - } - - vertex.position.x += chunk->x * CHUNK_W + 0.5f; - vertex.position.y += 0.5f; - vertex.position.z += chunk->z * CHUNK_D + 0.5f; - } - sortingMesh.entries.push_back(std::move(entry)); - vertexCount = 0; - vertexOffset = indexCount = 0; + if (!def.translucent) { + continue; } + const UVRegion texfaces[6] { + cache.getRegion(id, variantId, 0, densePass), + cache.getRegion(id, variantId, 1, densePass), + cache.getRegion(id, variantId, 2, densePass), + cache.getRegion(id, variantId, 3, densePass), + cache.getRegion(id, variantId, 4, densePass), + cache.getRegion(id, variantId, 5, densePass) + }; + int x = i % CHUNK_W; + int y = i / (CHUNK_D * CHUNK_W); + int z = (i / CHUNK_D) % CHUNK_W; + switch (def.getModel(state.userbits).type) { + case BlockModelType::BLOCK: + blockCube({x, y, z}, texfaces, def, vox.state, !def.shadeless, + def.ambientOcclusion && enableAO); + break; + case BlockModelType::XSPRITE: { + blockXSprite(x, y, z, glm::vec3(1.0f), + texfaces[FACE_MX], texfaces[FACE_MZ], 1.0f); + break; + } + case BlockModelType::AABB: { + blockAABB( + {x, y, z}, + texfaces, + &def, + vox.state.rotation, + !def.shadeless, + def.ambientOcclusion && enableAO + ); + break; + } + case BlockModelType::CUSTOM: { + blockCustomModel( + {x, y, z}, + def, + vox.state, + !def.shadeless, + def.ambientOcclusion && enableAO + ); + break; + } + default: + break; + } + if (vertexCount == 0) { + continue; + } + SortingMeshEntry entry { + glm::vec3( + x + chunk->x * CHUNK_W + 0.5f, + y + 0.5f, + z + chunk->z * CHUNK_D + 0.5f + ), + util::Buffer(indexCount), 0}; + + totalSize += entry.vertexData.size(); + + for (int j = 0; j < indexCount; j++) { + std::memcpy( + entry.vertexData.data() + j, + vertexBuffer.get() + indexBuffer[j], + sizeof(ChunkVertex) + ); + ChunkVertex& vertex = entry.vertexData[j]; + + if (!aabbInit) { + aabbInit = true; + aabb.a = aabb.b = vertex.position; + } else { + aabb.addPoint(vertex.position); + } + + vertex.position.x += chunk->x * CHUNK_W + 0.5f; + vertex.position.y += 0.5f; + vertex.position.z += chunk->z * CHUNK_D + 0.5f; + } + sortingMesh.entries.push_back(std::move(entry)); + vertexCount = 0; + vertexOffset = indexCount = 0; } // additional powerful optimization @@ -710,19 +695,16 @@ void BlocksRenderer::build( int totalBegin = chunk->bottom * (CHUNK_W * CHUNK_D); int totalEnd = chunk->top * (CHUNK_W * CHUNK_D); + bool hasTranslucent = false; - int beginEnds[256][2] {}; for (int i = totalBegin; i < totalEnd; i++) { const voxel& vox = voxels[i]; blockid_t id = vox.id; const auto& def = *blockDefsCache[id]; - const auto& variant = def.getVariantByBits(vox.state.userbits); - hasTranslucent = def.translucent || hasTranslucent; - - if (beginEnds[variant.drawGroup][0] == 0) { - beginEnds[variant.drawGroup][0] = i + 1; + if (def.translucent) { + hasTranslucent = true; + break; } - beginEnds[variant.drawGroup][1] = i; } cancelled = false; @@ -734,7 +716,7 @@ void BlocksRenderer::build( densePass = false; if (hasTranslucent) { - sortingMesh = renderTranslucent(voxels, beginEnds); + sortingMesh = renderTranslucent(voxels, totalBegin, totalEnd); } else { sortingMesh = {}; } @@ -747,13 +729,13 @@ void BlocksRenderer::build( denseRender = false; densePass = false; - render(voxels, beginEnds); + render(voxels, totalBegin, totalEnd); size_t endIndex = indexCount; denseRender = true; densePass = settings.graphics.denseRender.get(); - render(voxels, beginEnds); + render(voxels, totalBegin, totalEnd); denseIndexCount = indexCount; for (size_t i = 0; i < denseIndexCount; i++) { @@ -762,7 +744,7 @@ void BlocksRenderer::build( indexCount = endIndex; densePass = false; - render(voxels, beginEnds); + render(voxels, totalBegin, totalEnd); } ChunkMeshData BlocksRenderer::createMesh() { diff --git a/src/graphics/render/BlocksRenderer.hpp b/src/graphics/render/BlocksRenderer.hpp index 94b708a54..0488f0080 100644 --- a/src/graphics/render/BlocksRenderer.hpp +++ b/src/graphics/render/BlocksRenderer.hpp @@ -25,7 +25,7 @@ class BlocksRenderer final { public: BlocksRenderer( size_t capacity, - const Content& content, + const Block* const* blockDefs, const ContentGfxCache& cache, const EngineSettings& settings ); @@ -44,7 +44,6 @@ public: } private: static const glm::vec3 SUN_VECTOR; - const Content& content; std::unique_ptr vertexBuffer; std::unique_ptr indexBuffer; std::unique_ptr denseIndexBuffer; @@ -145,13 +144,16 @@ private: ); // Does block allow to see other blocks sides (is it transparent) - inline bool isOpen(const glm::ivec3& pos, const Block& def, const Variant& variant) const { + bool isOpen(const glm::ivec3& pos, const Block& def, const Variant& variant) const { const auto& vox = voxelsBuffer->pickBlock( chunk->x * CHUNK_W + pos.x, pos.y, chunk->z * CHUNK_D + pos.z ); if (vox.id == BLOCK_VOID) { return false; } + if (def.rt.id == vox.id && def.rt.solid && variant.culling == CullingMode::DEFAULT) { + return false; + } const auto& block = *blockDefsCache[vox.id]; const auto& blockVariant = block.getVariantByBits(vox.state.userbits); uint8_t otherDrawGroup = blockVariant.drawGroup; @@ -178,6 +180,6 @@ private: float x, float y, float z, const glm::ivec3& right, const glm::ivec3& up ) const; - void render(const voxel* voxels, const int beginEnds[256][2]); - SortingMeshData renderTranslucent(const voxel* voxels, int beginEnds[256][2]); + void render(const voxel* voxels, int totalBegin, int totalEnd); + SortingMeshData renderTranslucent(const voxel* voxels, int totalBegin, int totalEnd); }; diff --git a/src/graphics/render/ChunksRenderer.cpp b/src/graphics/render/ChunksRenderer.cpp index baf4cf356..7e81e0bd5 100644 --- a/src/graphics/render/ChunksRenderer.cpp +++ b/src/graphics/render/ChunksRenderer.cpp @@ -14,6 +14,7 @@ #include "util/listutil.hpp" #include "util/ObjectsPool.hpp" #include "settings.hpp" +#include "content/Content.hpp" static debug::Logger logger("chunks-render"); @@ -33,7 +34,7 @@ public: settings.graphics.denseRender.get() ? settings.graphics.chunkMaxVerticesDense.get() : settings.graphics.chunkMaxVertices.get(), - level.content, + level.content.getIndices()->blocks.getDefs(), cache, settings ) { @@ -92,7 +93,7 @@ ChunksRenderer::ChunksRenderer( threadPool.setStopOnFail(false); renderer = std::make_unique( settings.graphics.chunkMaxVertices.get(), - level.content, cache, settings + level.content.getIndices()->blocks.getDefs(), cache, settings ); logger.info() << "created " << threadPool.getWorkersCount() << " workers"; logger.info() << "memory consumption is " @@ -126,7 +127,7 @@ const ChunkMesh* ChunksRenderer::render( const std::shared_ptr& chunk, bool important, bool lowPriority ) { glm::ivec2 key(chunk->x, chunk->z); - if (important) { + if (important || true) { ChunkMesh mesh {}; auto voxelsBuffer = prepareVoxelsVolume(*chunk); mesh = renderer->render(chunk.get(), *voxelsBuffer); diff --git a/src/voxels/VoxelsVolume.hpp b/src/voxels/VoxelsVolume.hpp index 471b159c5..d90acfa5d 100644 --- a/src/voxels/VoxelsVolume.hpp +++ b/src/voxels/VoxelsVolume.hpp @@ -66,7 +66,7 @@ public: return lights.get(); } - inline blockid_t pickBlockId(int bx, int by, int bz) const { + blockid_t pickBlockId(int bx, int by, int bz) const { if (bx < x || by < y || bz < z || bx >= x + w || by >= y + h || bz >= z + d) { return BLOCK_VOID; @@ -75,7 +75,7 @@ public: } - inline voxel pickBlock(int bx, int by, int bz) const { + voxel pickBlock(int bx, int by, int bz) const { if (bx < x || by < y || bz < z || bx >= x + w || by >= y + h || bz >= z + d) { return {BLOCK_VOID, {}}; @@ -83,7 +83,7 @@ public: return voxels[vox_index(bx - x, by - y, bz - z, w, d)]; } - inline light_t pickLight(int bx, int by, int bz) const { + light_t pickLight(int bx, int by, int bz) const { if (bx < x || by < y || bz < z || bx >= x + w || by >= y + h || bz >= z + d) { return 0; @@ -103,10 +103,10 @@ private: template class StaticVoxelsVolume { public: - static inline constexpr size_t size = w * h * d; - static inline constexpr int width = w; - static inline constexpr int height = h; - static inline constexpr int depth = d; + static constexpr size_t size = w * h * d; + static constexpr int width = w; + static constexpr int height = h; + static constexpr int depth = d; StaticVoxelsVolume() : StaticVoxelsVolume(0, 0, 0) {} @@ -142,7 +142,7 @@ public: return lights; } - inline blockid_t pickBlockId(uint bx, uint by, uint bz) const { + blockid_t pickBlockId(uint bx, uint by, uint bz) const { bx -= x; by -= y; bz -= z; @@ -153,7 +153,7 @@ public: } - inline const voxel& pickBlock(uint bx, uint by, uint bz) const { + const voxel& pickBlock(uint bx, uint by, uint bz) const { bx -= x; by -= y; bz -= z; @@ -164,7 +164,7 @@ public: return voxels[vox_index(bx, by, bz, w, d)]; } - inline light_t pickLight(uint bx, uint by, uint bz) const { + light_t pickLight(uint bx, uint by, uint bz) const { bx -= x; by -= y; bz -= z; From 5b868ae857b1b425a4f529c663b3dad55fcbf59a Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 14 Jul 2026 19:03:32 +0300 Subject: [PATCH 048/217] fix graphics settings & reduce extra GL calls --- src/graphics/core/Batch2D.cpp | 2 +- src/graphics/core/Batch3D.cpp | 4 ++-- src/graphics/core/LineBatch.cpp | 2 +- src/graphics/core/Mesh.hpp | 11 ++++++++--- src/graphics/core/Mesh.inl | 15 ++++++++------- src/graphics/render/MainBatch.cpp | 2 +- src/graphics/render/WorldRenderer.hpp | 2 +- 7 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/graphics/core/Batch2D.cpp b/src/graphics/core/Batch2D.cpp index 7aa856f3f..33265b49f 100644 --- a/src/graphics/core/Batch2D.cpp +++ b/src/graphics/core/Batch2D.cpp @@ -372,7 +372,7 @@ void Batch2D::sprite( void Batch2D::flush() { if (index == 0) return; - mesh->reload(buffer.get(), index); + mesh->reload(buffer.get(), index, true); mesh->draw(gl::to_glenum(primitive)); index = 0; } diff --git a/src/graphics/core/Batch3D.cpp b/src/graphics/core/Batch3D.cpp index ce9f176c7..3aa996706 100644 --- a/src/graphics/core/Batch3D.cpp +++ b/src/graphics/core/Batch3D.cpp @@ -292,13 +292,13 @@ void Batch3D::point(const glm::vec3& coord, const glm::vec4& tint) { } void Batch3D::flush() { - mesh->reload(buffer.get(), index); + mesh->reload(buffer.get(), index, true); mesh->draw(); index = 0; } void Batch3D::flushPoints() { - mesh->reload(buffer.get(), index); + mesh->reload(buffer.get(), index, true); mesh->draw(GL_POINTS); index = 0; } diff --git a/src/graphics/core/LineBatch.cpp b/src/graphics/core/LineBatch.cpp index 9e33398ac..514f550d6 100644 --- a/src/graphics/core/LineBatch.cpp +++ b/src/graphics/core/LineBatch.cpp @@ -57,7 +57,7 @@ void LineBatch::box(float x, float y, float z, float w, float h, float d, void LineBatch::flush(){ if (index == 0) return; - mesh->reload(buffer.get(), index); + mesh->reload(buffer.get(), index, true); mesh->draw(GL_LINES); index = 0; } diff --git a/src/graphics/core/Mesh.hpp b/src/graphics/core/Mesh.hpp index 48c029696..2ff15cc2a 100644 --- a/src/graphics/core/Mesh.hpp +++ b/src/graphics/core/Mesh.hpp @@ -47,12 +47,15 @@ public: void reload( const VertexStructure* vertexBuffer, size_t vertexCount, - const std::vector& indices + const std::vector& indices, + bool streaming ); - void reload(const VertexStructure* vertexBuffer, size_t vertexCount) { + void reload( + const VertexStructure* vertexBuffer, size_t vertexCount, bool streaming + ) { static const std::vector indices {}; - reload(vertexBuffer, vertexCount, indices); + reload(vertexBuffer, vertexCount, indices, streaming); } /// @brief Draw mesh with specified primitives type @@ -61,6 +64,8 @@ public: /// @brief Draw mesh as triangles void draw() const; +private: + }; #include "graphics/core/Mesh.inl" diff --git a/src/graphics/core/Mesh.inl b/src/graphics/core/Mesh.inl index 913da2bce..4c6424dd1 100644 --- a/src/graphics/core/Mesh.inl +++ b/src/graphics/core/Mesh.inl @@ -46,9 +46,9 @@ Mesh::Mesh( glGenVertexArrays(1, &vao); glGenBuffers(1, &vbo); - reload(vertexBuffer, vertices, std::move(indices)); - glBindVertexArray(vao); + reload(vertexBuffer, vertices, std::move(indices), false); + // attributes int offset = 0; for (int i = 0; attrs[i].count; i++) { @@ -82,20 +82,22 @@ template void Mesh::reload( const VertexStructure* vertexBuffer, size_t vertexCount, - const std::vector& indices + const std::vector& indices, + bool streaming ) { this->vertexCount = vertexCount; - glBindVertexArray(vao); glBindBuffer(GL_ARRAY_BUFFER, vbo); if (vertexBuffer != nullptr && vertexCount != 0) { glBufferData( GL_ARRAY_BUFFER, vertexCount * sizeof(VertexStructure), vertexBuffer, - GL_STREAM_DRAW + streaming ? GL_STREAM_DRAW : GL_STATIC_DRAW ); } else { - glBufferData(GL_ARRAY_BUFFER, 0, {}, GL_STREAM_DRAW); + glBufferData( + GL_ARRAY_BUFFER, 0, {}, streaming ? GL_STREAM_DRAW : GL_STATIC_DRAW + ); } for (int i = 0; i < ibos.size(); i++) { @@ -116,7 +118,6 @@ void Mesh::reload( GL_STATIC_DRAW ); } - glBindVertexArray(0); } template diff --git a/src/graphics/render/MainBatch.cpp b/src/graphics/render/MainBatch.cpp index 1a501ca15..91364e510 100644 --- a/src/graphics/render/MainBatch.cpp +++ b/src/graphics/render/MainBatch.cpp @@ -46,7 +46,7 @@ void MainBatch::flush() { texture = blank.get(); } texture->bind(); - mesh->reload(buffer.get(), index); + mesh->reload(buffer.get(), index, true); mesh->draw(); index = 0; } diff --git a/src/graphics/render/WorldRenderer.hpp b/src/graphics/render/WorldRenderer.hpp index 49add0351..b3b10f88c 100644 --- a/src/graphics/render/WorldRenderer.hpp +++ b/src/graphics/render/WorldRenderer.hpp @@ -83,7 +83,7 @@ private: bool debug = false; bool lightsDebug = false; bool gbufferPipeline = false; - bool dirtySettings = false; + bool dirtySettings = true; /// @brief Render block selection lines void renderBlockSelection(); From 17f3cbce954fe14f0a789f444a5db09f927e068d Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 14 Jul 2026 20:12:56 +0300 Subject: [PATCH 049/217] update Device.hpp --- src/io/devices/Device.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/io/devices/Device.hpp b/src/io/devices/Device.hpp index cfbe4b1e0..4cad4c5de 100644 --- a/src/io/devices/Device.hpp +++ b/src/io/devices/Device.hpp @@ -2,7 +2,7 @@ #include #include -#include +#include #include #include "../path.hpp" From 413004d7311f382373a8983fc6783159de4cb438 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 14 Jul 2026 23:59:09 +0300 Subject: [PATCH 050/217] test dithering solution --- res/shaders/entity.glslf | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/res/shaders/entity.glslf b/res/shaders/entity.glslf index f3293adb7..d2eb78f4d 100644 --- a/res/shaders/entity.glslf +++ b/res/shaders/entity.glslf @@ -15,11 +15,32 @@ uniform float u_fogCurve; uniform bool u_alphaClip; uniform vec3 u_sunDir; +const float BAYER_MATRIX[64] = float[]( + 0.0, 32.0, 8.0, 40.0, 2.0, 34.0, 10.0, 42.0, + 48.0, 16.0, 56.0, 24.0, 50.0, 18.0, 58.0, 26.0, + 12.0, 44.0, 4.0, 36.0, 14.0, 46.0, 6.0, 38.0, + 60.0, 28.0, 52.0, 20.0, 62.0, 30.0, 54.0, 22.0, + 3.0, 35.0, 11.0, 43.0, 1.0, 33.0, 9.0, 41.0, + 51.0, 19.0, 59.0, 27.0, 49.0, 17.0, 57.0, 25.0, + 15.0, 47.0, 7.0, 39.0, 13.0, 45.0, 5.0, 37.0, + 63.0, 31.0, 55.0, 23.0, 61.0, 29.0, 53.0, 21.0 +); + +float calc_bayer(vec2 pos){ + ivec2 p = ivec2(mod(pos, 8.0)); + return BAYER_MATRIX[p.y * 8 + p.x] / 64.0; +} + void main() { vec4 texColor = texture(u_texture0, a_texCoord); float alpha = a_color.a * texColor.a; // anyway it's any alpha-test alternative required - if (alpha < (u_alphaClip ? 0.5f : 0.15f)) { + if (u_alphaClip) { + float bayer = calc_bayer(gl_FragCoord.xy); + if (alpha * bayer * bayer < 0.1f) { + discard; + } + } else if (alpha < 0.05f) { discard; } f_color = a_color * texColor; From 08fae1a47c343615fe12fb80cc256a9f81ee94fe Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 15 Jul 2026 00:38:54 +0300 Subject: [PATCH 051/217] fix --- res/shaders/entity.glslf | 1 + 1 file changed, 1 insertion(+) diff --git a/res/shaders/entity.glslf b/res/shaders/entity.glslf index d2eb78f4d..5620ef20a 100644 --- a/res/shaders/entity.glslf +++ b/res/shaders/entity.glslf @@ -40,6 +40,7 @@ void main() { if (alpha * bayer * bayer < 0.1f) { discard; } + alpha = 1.0; } else if (alpha < 0.05f) { discard; } From db4f05cf36ade3ddad63cb662bef151d8950cbf9 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 15 Jul 2026 00:47:15 +0300 Subject: [PATCH 052/217] update entity.glslf --- res/shaders/entity.glslf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/shaders/entity.glslf b/res/shaders/entity.glslf index 5620ef20a..a817d963a 100644 --- a/res/shaders/entity.glslf +++ b/res/shaders/entity.glslf @@ -37,7 +37,7 @@ void main() { // anyway it's any alpha-test alternative required if (u_alphaClip) { float bayer = calc_bayer(gl_FragCoord.xy); - if (alpha * bayer * bayer < 0.1f) { + if (alpha - bayer < 0.01f) { discard; } alpha = 1.0; From a6c4e9e22e5696a09aec48d46064c260fa00938c Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 15 Jul 2026 19:28:02 +0300 Subject: [PATCH 053/217] reduce fog factor for emissive geometry --- res/shaders/effects/deferred_lighting.glsl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/shaders/effects/deferred_lighting.glsl b/res/shaders/effects/deferred_lighting.glsl index c314df02a..6207f1583 100644 --- a/res/shaders/effects/deferred_lighting.glsl +++ b/res/shaders/effects/deferred_lighting.glsl @@ -32,6 +32,6 @@ vec4 effect() { light = pow(light, u_gamma); vec3 fogColor = texture(u_skybox, dir).rgb; - float fog = calc_fog(length(u_view * vec4((modelpos.xyz - u_cameraPos) * FOG_POS_SCALE, 0.0)) / 256.0); + float fog = calc_fog(pow(length(u_view * vec4((modelpos.xyz - u_cameraPos) * FOG_POS_SCALE, 0.0)) / 256.0, (1.0f - emission * 0.7))); return vec4(mix(texture(u_screen, v_uv).rgb * mix(1.0, light, 1.0), fogColor, fog), 1.0); } From 86096553ce847768cc1b17ebf7b27f15a76ecb25 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sat, 18 Jul 2026 16:20:10 +0300 Subject: [PATCH 054/217] fix --- res/shaders/effects/deferred_lighting.glsl | 9 ++++++++- res/shaders/lib/fog.glsl | 8 ++++++-- src/graphics/render/CloudsRenderer.cpp | 2 +- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/res/shaders/effects/deferred_lighting.glsl b/res/shaders/effects/deferred_lighting.glsl index 6207f1583..55e267bf3 100644 --- a/res/shaders/effects/deferred_lighting.glsl +++ b/res/shaders/effects/deferred_lighting.glsl @@ -1,6 +1,9 @@ #include #include +#define CLOUDS_FOG_FACTOR_MUL 0.3f +#define CLOUDS_FOG_CURVE_MUL 0.4f + vec4 effect() { vec4 pos = texture(u_position, v_uv); float light = 1.0; @@ -32,6 +35,10 @@ vec4 effect() { light = pow(light, u_gamma); vec3 fogColor = texture(u_skybox, dir).rgb; - float fog = calc_fog(pow(length(u_view * vec4((modelpos.xyz - u_cameraPos) * FOG_POS_SCALE, 0.0)) / 256.0, (1.0f - emission * 0.7))); + float fog = calc_fog( + length(u_view * vec4((modelpos.xyz - u_cameraPos) * FOG_POS_SCALE, 0.0)) / 256.0, + mix(1.0, CLOUDS_FOG_FACTOR_MUL, emission), + mix(1.0, CLOUDS_FOG_CURVE_MUL, emission) + ); return vec4(mix(texture(u_screen, v_uv).rgb * mix(1.0, light, 1.0), fogColor, fog), 1.0); } diff --git a/res/shaders/lib/fog.glsl b/res/shaders/lib/fog.glsl index e5ff18bbd..597380bb6 100644 --- a/res/shaders/lib/fog.glsl +++ b/res/shaders/lib/fog.glsl @@ -7,13 +7,17 @@ uniform float u_weatherFogOpacity; uniform float u_weatherFogDencity; uniform float u_weatherFogCurve; -float calc_fog(float depth) { +float calc_fog(float depth, float fogFactor, float fogCurve) { return min( 1.0, - max(pow(depth * u_fogFactor, u_fogCurve), + max(pow(depth * u_fogFactor * fogFactor, u_fogCurve * fogCurve), min(pow(depth * u_weatherFogDencity, u_weatherFogCurve), u_weatherFogOpacity)) ); } +float calc_fog(float depth) { + return calc_fog(depth, 1.0, 1.0); +} + #endif // FOG_GLSL_ diff --git a/src/graphics/render/CloudsRenderer.cpp b/src/graphics/render/CloudsRenderer.cpp index e2047eb63..bfc53e4bb 100755 --- a/src/graphics/render/CloudsRenderer.cpp +++ b/src/graphics/render/CloudsRenderer.cpp @@ -329,7 +329,7 @@ void CloudsRenderer::draw( shader.uniform4f("u_tint", glm::vec4(weather.cloudsTint(), 1.0f)); shader.uniform1f("u_fogFactor", fogFactor * 0.03f); - shader.uniform1f("u_fogCurve", 0.7f - 0.3f); + shader.uniform1f("u_fogCurve", 0.4f); for (int i = 0; i < std::min(quality, layers.size()); i++) { draw(layers[i], frustum, shader, camera, timer, i); From a1a5d390c4207f7b108259161438338a99f130e6 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 21 Jul 2026 22:41:03 +0300 Subject: [PATCH 055/217] add math.sign --- res/modules/internal/extensions/math.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/res/modules/internal/extensions/math.lua b/res/modules/internal/extensions/math.lua index d906e94a2..df0eadf72 100644 --- a/res/modules/internal/extensions/math.lua +++ b/res/modules/internal/extensions/math.lua @@ -35,3 +35,7 @@ function math.sum(...) return sum end + +function math.sign(x) + return (x > 0) and 1 or (x < 0 and -1 or 0) +end From bed71ea607da4804061ea229970dec296ed424f0 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 21 Jul 2026 22:43:50 +0300 Subject: [PATCH 056/217] update doc/ru/scripting/extensions.md --- doc/ru/scripting/extensions.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/ru/scripting/extensions.md b/doc/ru/scripting/extensions.md index 868210056..e7b0fe292 100644 --- a/doc/ru/scripting/extensions.md +++ b/doc/ru/scripting/extensions.md @@ -156,6 +156,8 @@ math.round(num: number, [опционально] places: number) -> number -- Возвращает сумму всех принимаемых аргументов. Если в качестве аргумента была передана таблица, метод вернёт сумму всех её элементов. math.sum(x: number, ... | t: table) -> number +-- Возвращает целое число, указывающее знак числа (-1/0/1) +math.sign(x: number) -> int ``` ## Расширения для bit @@ -200,4 +202,4 @@ await(co: coroutine) -> result, error -- Константа, в которой хранится PID текущего инстанса движка. os.pid -> number -``` \ No newline at end of file +``` From 2d1e783a64a0e352c2cb2d3c4e05b9de001d51f5 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 21 Jul 2026 22:49:10 +0300 Subject: [PATCH 057/217] add entities.def_solid --- src/logic/scripting/lua/libs/libentity.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/logic/scripting/lua/libs/libentity.cpp b/src/logic/scripting/lua/libs/libentity.cpp index 64e8e126e..110aee123 100644 --- a/src/logic/scripting/lua/libs/libentity.cpp +++ b/src/logic/scripting/lua/libs/libentity.cpp @@ -50,6 +50,13 @@ static int l_def_hitbox(lua::State* L) { return 0; } +static int l_def_solid(lua::State* L) { + if (auto def = require_entity_def(L)) { + return lua::pushboolean(L, def->solid); + } + return 0; +} + static int l_defs_count(lua::State* L) { return lua::pushinteger(L, indices->entities.count()); } @@ -373,6 +380,7 @@ const luaL_Reg entitylib[] = { {"def_index", lua::wrap}, {"def_name", lua::wrap}, {"def_hitbox", lua::wrap}, + {"def_solid", lua::wrap}, {"get_def", lua::wrap}, {"defs_count", lua::wrap}, {"spawn", lua::wrap}, From e829bf7912d6776f2db2173cbb3b481aa0369d19 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 21 Jul 2026 22:49:38 +0300 Subject: [PATCH 058/217] update doc/*/scripting/builtins/libentities.md --- doc/en/scripting/builtins/libentities.md | 3 +++ doc/ru/scripting/builtins/libentities.md | 3 +++ 2 files changed, 6 insertions(+) diff --git a/doc/en/scripting/builtins/libentities.md b/doc/en/scripting/builtins/libentities.md index e34cd7a53..375c52660 100644 --- a/doc/en/scripting/builtins/libentities.md +++ b/doc/en/scripting/builtins/libentities.md @@ -30,6 +30,9 @@ entities.def_hitbox(id: int) -> vec3 -- Returns entity definition name by index (string ID). entities.def_name(id: int) -> str +-- Returns true if the entity is a solid obstacle. +entities.def_solid(id: int) -> boolean + -- Returns entity definition index by name (integer ID). entities.def_index(name: str) -> int diff --git a/doc/ru/scripting/builtins/libentities.md b/doc/ru/scripting/builtins/libentities.md index 27d8d03b1..d14ef7ea6 100644 --- a/doc/ru/scripting/builtins/libentities.md +++ b/doc/ru/scripting/builtins/libentities.md @@ -30,6 +30,9 @@ entities.def_name(id: int) -> string -- Возвращает значение свойства 'hitbox' сущности entities.def_hitbox(id: int) -> vec3 +-- Возвращает true если сущность является осязаемым препятствием. +entities.def_solid(id: int) -> boolean + -- Возвращает индекс определения сущности по имени (числовой ID). entities.def_index(name: string) -> int From 3c7b7a931890cdd55941d27f33fa05de7e3b71af Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 21 Jul 2026 22:53:22 +0300 Subject: [PATCH 059/217] update doc/*/scripting/builtins/libentities.md --- doc/en/scripting/builtins/libentities.md | 4 ++-- doc/ru/scripting/builtins/libentities.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/en/scripting/builtins/libentities.md b/doc/en/scripting/builtins/libentities.md index 375c52660..9e653ba78 100644 --- a/doc/en/scripting/builtins/libentities.md +++ b/doc/en/scripting/builtins/libentities.md @@ -54,12 +54,12 @@ entities.get_all() -> table -- Returns a table of loaded entities based on the passed list of UIDs entities.get_all(uids: array) -> table --- Returns a list of UIDs of entities inside the rectangular area +-- Returns a list of UIDs of entities with origin inside the rectangular area -- pos - minimal area corner -- size - area size entities.get_all_in_box(pos: vec3, size: vec3) -> array --- Returns a list of UIDs of entities inside the radius +-- Returns a list of UIDs of entities with origin inside the radius -- center - center of the area -- radius - radius of the area entities.get_all_in_radius(center: vec3, radius: number) -> array diff --git a/doc/ru/scripting/builtins/libentities.md b/doc/ru/scripting/builtins/libentities.md index d14ef7ea6..9753c7e15 100644 --- a/doc/ru/scripting/builtins/libentities.md +++ b/doc/ru/scripting/builtins/libentities.md @@ -53,12 +53,12 @@ entities.get_all() -> table -- Возвращает таблицу загруженных сущностей по переданному списку UID entities.get_all(uids: table) -> table --- Возвращает список UID сущностей, попадающих в прямоугольную область +-- Возвращает список UID сущностей, позиции которых попадают в прямоугольную область -- pos - минимальный угол области -- size - размер области entities.get_all_in_box(pos: vec3, size: vec3) -> table --- Возвращает список UID сущностей, попадающих в радиус +-- Возвращает список UID сущностей, позиции которых попадают в радиус -- center - центр области -- radius - радиус области entities.get_all_in_radius(center: vec3, radius: number) -> table From c023697bccc0fddc94eef79a85ec51860757c704 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 21 Jul 2026 23:13:09 +0300 Subject: [PATCH 060/217] update doc/*/scripting/ecs.md --- doc/en/scripting/ecs.md | 11 +++++++++++ doc/ru/scripting/ecs.md | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/doc/en/scripting/ecs.md b/doc/en/scripting/ecs.md index 52be70363..9636cf763 100644 --- a/doc/en/scripting/ecs.md +++ b/doc/en/scripting/ecs.md @@ -38,6 +38,17 @@ entity:set_enabled(name: str, enable: bool) entity:get_player() -> int or nil ``` +## Custom Components + +A component is defined as a script in `{pack}/scripts/components/{name}.lua`. +The component will be available for use in the entity definition as `{pack}:{name}`. +Example: `core:scripts/components/pathfinding.lua` -> `core:pathfinding`. + +For **each** entity containing a component, a **separate instance** is created with **its own namespace** within the pack namespace. +Global variables declared in it act as public fields of the component (same for global functions). + +Entity components are specified via the ["components" list](../entity-properties.md#components) + ## Built-in components ### Transform diff --git a/doc/ru/scripting/ecs.md b/doc/ru/scripting/ecs.md index d659f342d..cb4a79bec 100644 --- a/doc/ru/scripting/ecs.md +++ b/doc/ru/scripting/ecs.md @@ -180,6 +180,17 @@ rig:get_color() -> vec3 rig:set_color(color: vec3) ``` +## Пользовательские компоненты + +Компонент описывается в виде скрипта `{пак}/scripts/components/{имя}.lua`. +Компонент будет доступен для использования в описании сущности как `{пак}:{имя}`. +Пример: `core:scripts/components/pathfinding.lua` -> `core:pathfinding`. + +Для **каждой** сущности, содержащей компонент, создаётся **отдельный экземпляр** со **своим пространством имен** внутри пространства имён пака. +Глобальные переменные, объявляемые в нём, играют роль публичных полей компонента (как и глобальные функции). + +Компоненты сущности указываются через [список "components"](../entity-properties.md#cписок-компонентов---components) + > [!WARNING] > При выходе сущности за пределы зоны прогрузки она удаляется, вызывая события. > При повторном попадании в зону прогрузки сущность спавнится заново. From c3794191e226a4cd587ec0b846ddb0aae1865370 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 22 Jul 2026 00:00:41 +0300 Subject: [PATCH 061/217] document 'script-name' propertyfor items --- doc/en/item-properties.md | 7 ++++++- doc/ru/item-properties.md | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/doc/en/item-properties.md b/doc/en/item-properties.md index 7bef07664..d3ae82e7d 100644 --- a/doc/en/item-properties.md +++ b/doc/en/item-properties.md @@ -60,13 +60,18 @@ Property used via [inventory.use](scripting/builtins/libinventory.md). Property status is displayed in the inventory interface. Display method is defined via `uses-display`. -### Display of uses - `uses-display` +### Display of remaining uses count - `uses-display` - `none` - display disabled - `number` - number - `relation` - current value to initial value (x/y) - `vbar` - vertical scale (used by default) +### *script-name* + +Defines the name of the script containing the item's event handlers. By default, equals to the item's string id (name). +Allows you to reuse a single script for multiple items. + ## Tags Tags allow you to designate general properties of items. Names should be formatted as `prefix:tag_name`. diff --git a/doc/ru/item-properties.md b/doc/ru/item-properties.md index cb45827a1..7453789fb 100644 --- a/doc/ru/item-properties.md +++ b/doc/ru/item-properties.md @@ -66,6 +66,10 @@ - `relation` - отношение текущего значения к изначальному (x/y) - `vbar` - вертикальная шкала (используется по-умолчанию) +### Имя скрипта - `script-name` + +Определяет имя скрипта, содержащего обработчики событий предмета. По-умолчанию соответствует строковому id (имени) предмета. +Свойство обеспечивает возможность использования одного скрипта для нескольких предметов. ## Теги - *tags* From da6a36a76c9208e94469f1e314bc019a501aaf19 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 22 Jul 2026 00:33:36 +0300 Subject: [PATCH 062/217] update scripting docs --- doc/en/scripting.md | 55 +++++++++++++++++++++++++++++++++++++++--- doc/en/scripting/ui.md | 2 +- doc/ru/scripting.md | 54 ++++++++++++++++++++++++++++++++++++++++- doc/ru/scripting/ui.md | 2 +- 4 files changed, 107 insertions(+), 6 deletions(-) diff --git a/doc/en/scripting.md b/doc/en/scripting.md index a21b959ed..7cd22ed8a 100644 --- a/doc/en/scripting.md +++ b/doc/en/scripting.md @@ -58,9 +58,58 @@ not part of Lua syntax. - quat - array of four numbers - quaternion - matrix - array of 16 numbers - matrix -## Core functions +## Namespaces + +Currently, the engine uses a namespace hierarchy that minimizes conflicts between individual modules, scripts, and even packs. + +The following structure is relevant: + +- Global namespace (_G), which is the root namespace. Writing to it outside of engine core modules is highly undesirable, as it can waste hours of debugging time. + - Pack namespace - created each time content is loaded for each pack included in the configuration. This namespace is used by modules, as well as item and block scripts. + - [Component namespace](scripting/ecs.md#built-in-components). + - [UI document namespace](scripting/ui.md). +- Isolated world generator namespace. + +## Modules + +A module is a global object with a lifetime limited to the lifetime of the loaded content, used both for interaction between different packages and as a substitute for global variables within the package itself. + +The module must be located in `contentpack/modules/**` (subfolders are allowed, but you will need to specify them) ```lua -require "packid:module_name" -- load Lua module from pack-folder/modules/ --- no extension included, just name +local module_name = require "contentpack:module_name" -- imports the module + +-- If the module is in the same pack in which it is imported, you can use the shortcut: +local module_name = require "module_name" + +-- When using nested folders: +local module_name = require "contentpack:path/to/module_name" -- the path does not include `modules` +local module_name = require "path/to/module_name" -- if in the same pack ``` + +The module will have the namespace of the folder in which it is located, regardless of the namespace from which the first import was performed. + +When creating a module, you should follow the following approach, which is what *require* expects: + +```lua +local this = { + variable_name = ... -- public variables +} + +-- private module variables +local variable_name = ... + +-- private module functions +local function function_name(...) + ... +end + +-- public module functions +function this.function_name(...) + ... +end + +return this -- the result that require will return and cache +``` + +When reimported, the module will not be reloaded; instead, require will return the cached result. diff --git a/doc/en/scripting/ui.md b/doc/en/scripting/ui.md index 04784b449..2d338a5aa 100644 --- a/doc/en/scripting/ui.md +++ b/doc/en/scripting/ui.md @@ -1,7 +1,7 @@ # UI properties and methods UI elements in scripts are accessed through a Document instance -(*document* variable) by id specified in xml. +(global *document* variable) by id specified in xml. Example: print the pos property of an element with id: "worlds-panel" to the console: ```lua diff --git a/doc/ru/scripting.md b/doc/ru/scripting.md index cd8a3b00a..db3fcd921 100644 --- a/doc/ru/scripting.md +++ b/doc/ru/scripting.md @@ -60,6 +60,58 @@ - quat - массив из четырех чисел - кватернион - matrix - массив из 16 чисел - матрица +## Пространства имён + +В настоящее время, движок использует иерархию пространств имён, минимизирующую конфликты между отдельными модулями, скриптами, да и паками. + +Актуальна следующая структура: + +- Глобальное пространство (оно же - _G), являющееся корневым, запись в которое, вне модулей ядра движка, является крайне нежелательным для вашего же времени, что может уйти на лишние часы отладки. + - Пространство пака - создаётся каждый раз при загрузке контента для каждого, включённого в конфигурацию, пака. Это пространство используют модули, а также скрипты предметов и блоков. + - Пространство [компонента](scripting/ecs.md#пользовательские-компоненты). + - Пространство [UI-документа](scripting/ui.md) +- Изолированное пространство имён генератора мира. + +## Модули + +Модуль - глобальный объект со сроком жизни, ограниченным сроком жизни контента, используемый как для взаимодействия разных паков, так и для вместо глобальных переменных в пределах самого пака. + +Модуль должен находиться в `контентпак/modules/**` (допускаются вложенные папки, что нужно будет указывать) + ```lua -require "контентпак:имя_модуля" -- загружает lua модуль из папки modules (расширение не указывается) +local имя_модуля = require "контентпак:имя_модуля" -- импортирует модуль + +-- если модуль находится в том же паке, в котором импортируется, можно использовать сокращённый вариант: +local имя_модуля = require "имя_модуля" + +-- при использовании вложенных папок: +local имя_модуля = require "контентпак:путь/к/имя_модуля" -- путь не включает `modules` +local имя_модуля = require "путь/к/имя_модуля" -- если в том же паке ``` + +Модуль будет иметь пространство имён того пака, в котором находится, независимо от пространства имён, из которого был выполнен первый импорт. + +При создании модуля следует придерживаться следующего подхода, которого и ожидает *require*: + +```lua +local this = { + имя_переменной = ... -- публичные переменные +} + +-- приватные переменные модуля +local имя_переменной = ... + +-- приватные функции модуля +local function имя_функции(...) + ... +end + +-- публичные функции модуля +function this.имя_функции(...) + ... +end + +return this -- результат, который и будет возвращать и кешировать require +``` + +При повторном импорте модуль не будет перезагружен - вместо этого require вернёт кешированный результат. diff --git a/doc/ru/scripting/ui.md b/doc/ru/scripting/ui.md index 6cff70e2e..631d5e00f 100644 --- a/doc/ru/scripting/ui.md +++ b/doc/ru/scripting/ui.md @@ -1,7 +1,7 @@ # Свойства и методы UI элементов Обращение к UI элементов в скриптах произодится через экземпляр Document -(переменная document) по id, указанному в xml. +(глобальная переменная document) по id, указанному в xml. Пример: вывод в консоль свойства pos элемента с id: "worlds-panel": ```lua From 29f5373d81853a1ac1397d0e07484978fa117100 Mon Sep 17 00:00:00 2001 From: RomanDonw Date: Wed, 22 Jul 2026 20:36:01 +1000 Subject: [PATCH 063/217] added bytearray view wrapper for float (float32) --- res/modules/internal/bytearray.lua | 2 ++ res/scripts/stdmin.lua | 1 + 2 files changed, 3 insertions(+) diff --git a/res/modules/internal/bytearray.lua b/res/modules/internal/bytearray.lua index 58b33a520..a90595979 100644 --- a/res/modules/internal/bytearray.lua +++ b/res/modules/internal/bytearray.lua @@ -305,6 +305,7 @@ local FFII16view = create_FFIview_class("FFII16view", "int16_t", 2) local FFIU16view = create_FFIview_class("FFIU16view", "uint16_t", 2) local FFII32view = create_FFIview_class("FFII32view", "int32_t", 4) local FFIU32view = create_FFIview_class("FFIU32view", "uint32_t", 4) +local FFIF32view = create_FFIview_class("FFIF32view", "float", 4) return { FFIBytearray = setmetatable(FFIBytearray, FFIBytearray), @@ -314,4 +315,5 @@ return { FFII16view = FFII16view, FFIU32view = FFIU32view, FFII32view = FFII32view, + FFIF32view = FFIF32view, } diff --git a/res/scripts/stdmin.lua b/res/scripts/stdmin.lua index 2868dd634..01b61efc6 100644 --- a/res/scripts/stdmin.lua +++ b/res/scripts/stdmin.lua @@ -314,6 +314,7 @@ U16view = bytearray.FFIU16view I16view = bytearray.FFII16view U32view = bytearray.FFIU32view I32view = bytearray.FFII32view +F32view = bytearray.FFIF32view Bytearray_construct = function(...) return Bytearray(...) end bit.compile = require "core:bitwise/compiler" From c7ba237933310866a7f7945e5a2d92cb777cd503 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 22 Jul 2026 21:55:53 +0300 Subject: [PATCH 064/217] add xml library --- src/coders/xml.cpp | 8 +++ src/coders/xml.hpp | 6 ++ src/logic/scripting/lua/libs/api_lua.hpp | 1 + src/logic/scripting/lua/libs/libxml.cpp | 90 ++++++++++++++++++++++++ src/logic/scripting/lua/lua_engine.cpp | 1 + 5 files changed, 106 insertions(+) create mode 100644 src/logic/scripting/lua/libs/libxml.cpp diff --git a/src/coders/xml.cpp b/src/coders/xml.cpp index 93e1c8031..b40ee7700 100644 --- a/src/coders/xml.cpp +++ b/src/coders/xml.cpp @@ -528,3 +528,11 @@ std::string xml::stringify( return ss.str(); } + +std::string xml::stringify( + const Node& element, bool nice, const std::string& indentStr +) { + std::stringstream ss; + stringifyElement(ss, element, nice, indentStr, 0); + return ss.str(); +} diff --git a/src/coders/xml.hpp b/src/coders/xml.hpp index 4954caa17..1a8a94ca5 100644 --- a/src/coders/xml.hpp +++ b/src/coders/xml.hpp @@ -119,6 +119,12 @@ namespace xml { const std::string& indentStr = " " ); + std::string stringify( + const Node& element, + bool nice = true, + const std::string& indentStr = " " + ); + /// @brief Read XML Document from string /// @param filename file name will be shown in error messages /// @param source xml source code string diff --git a/src/logic/scripting/lua/libs/api_lua.hpp b/src/logic/scripting/lua/libs/api_lua.hpp index e304d6dc1..6d135c100 100644 --- a/src/logic/scripting/lua/libs/api_lua.hpp +++ b/src/logic/scripting/lua/libs/api_lua.hpp @@ -53,6 +53,7 @@ extern const luaL_Reg vec3lib[]; // vecn.cpp extern const luaL_Reg vec4lib[]; // vecn.cpp extern const luaL_Reg weatherlib[]; // gfx.weather extern const luaL_Reg worldlib[]; +extern const luaL_Reg xmllib[]; extern const luaL_Reg yamllib[]; // Components diff --git a/src/logic/scripting/lua/libs/libxml.cpp b/src/logic/scripting/lua/libs/libxml.cpp new file mode 100644 index 000000000..e2121465d --- /dev/null +++ b/src/logic/scripting/lua/libs/libxml.cpp @@ -0,0 +1,90 @@ +#include "coders/xml.hpp" +#include "api_lua.hpp" + +static int push_xml(lua::State* L, const xml::xmlelement& elem) { + if (elem.isText()) { + return lua::pushlstring(L, elem.getInnerText()); + } + + const auto& attrs = elem.getAttributes(); + const auto& elems = elem.getElements(); + const auto& tag = elem.getTag(); + + lua::createtable(L, elem.size(), attrs.size() + 1); + lua::pushlstring(L, tag); + lua::setfield(L, "@"); + + for (int i = 0; i < elems.size(); i++) { + push_xml(L, *elems[i]); + lua::rawseti(L, i + 1); + } + + for (const auto& [key, value] : attrs) { + lua::pushlstring(L, value.getText()); + lua::setfield(L, key); + } + return 1; +} + +static std::unique_ptr toxml(lua::State* L) { + lua::getfield(L, "@"); + auto tag = lua::require_lstring(L, -1); + lua::pop(L); + + auto elem = std::make_unique(std::string(tag)); + int length = lua::objlen(L, -1); + + for (int i = 0; i < length; i++) { + lua::rawgeti(L, i + 1); + elem->add(toxml(L)); + lua::pop(L); + } + + lua::pushnil(L); + while (lua::next(L, -2)) { + lua::pushvalue(L, -2); + do { + if (!lua::isstring(L, -1)) { + break; + } + auto key = lua::require_lstring(L, -1); + if (key == "@") { + break; + } + auto value = lua::require_lstring(L, -2); + elem->set(std::string(key), std::string(value)); + } while (false); + + lua::pop(L, 2); + } + return elem; +} + +static int l_tostring(lua::State* L) { + bool nice = lua::isboolean(L, 2) ? lua::toboolean(L, 2) : true; + lua::pushvalue(L, 1); + auto xml = toxml(L); + lua::pop(L); + return lua::pushstring(L, xml::stringify(*xml, nice)); +} + +static int l_parse(lua::State* L) { + auto string = lua::require_string(L, 1); + auto document = xml::parse("[string]", string); + return push_xml(L, *document->getRoot()); +} + + +static int l_parse_vcd(lua::State* L) { + auto string = lua::require_string(L, 1); + auto rootTag = lua::tostring(L, 2); + auto document = xml::parse_vcm("[string]", string, rootTag ? rootTag : "root"); + return push_xml(L, *document->getRoot()); +} + +const luaL_Reg xmllib[] = { + {"tostring", lua::wrap}, + {"parse", lua::wrap}, + {"parse_vcd", lua::wrap}, + {nullptr, nullptr} +}; diff --git a/src/logic/scripting/lua/lua_engine.cpp b/src/logic/scripting/lua/lua_engine.cpp index afb2f42eb..276dbaa74 100644 --- a/src/logic/scripting/lua/lua_engine.cpp +++ b/src/logic/scripting/lua/lua_engine.cpp @@ -66,6 +66,7 @@ static void create_libs(State* L, StateType stateType) { openlib(L, "vec2", vec2lib); openlib(L, "vec3", vec3lib); openlib(L, "vec4", vec4lib); + openlib(L, "xml", xmllib); openlib(L, "yaml", yamllib); openlib(L, "__vc_app", applib); From cd53a33d7bc3ca5a8976bf1f37ad80e695f74b6d Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 22 Jul 2026 22:26:44 +0300 Subject: [PATCH 065/217] replace tag attribute key '@' with '#' --- src/logic/scripting/lua/libs/libxml.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/logic/scripting/lua/libs/libxml.cpp b/src/logic/scripting/lua/libs/libxml.cpp index e2121465d..ff52b6321 100644 --- a/src/logic/scripting/lua/libs/libxml.cpp +++ b/src/logic/scripting/lua/libs/libxml.cpp @@ -1,6 +1,9 @@ #include "coders/xml.hpp" #include "api_lua.hpp" +static const char* TAG_ATTR = "#"; +static const char* DEFAULT_ROOT_TAG = "root"; + static int push_xml(lua::State* L, const xml::xmlelement& elem) { if (elem.isText()) { return lua::pushlstring(L, elem.getInnerText()); @@ -12,7 +15,7 @@ static int push_xml(lua::State* L, const xml::xmlelement& elem) { lua::createtable(L, elem.size(), attrs.size() + 1); lua::pushlstring(L, tag); - lua::setfield(L, "@"); + lua::setfield(L, TAG_ATTR); for (int i = 0; i < elems.size(); i++) { push_xml(L, *elems[i]); @@ -27,7 +30,7 @@ static int push_xml(lua::State* L, const xml::xmlelement& elem) { } static std::unique_ptr toxml(lua::State* L) { - lua::getfield(L, "@"); + lua::getfield(L, TAG_ATTR); auto tag = lua::require_lstring(L, -1); lua::pop(L); @@ -78,7 +81,9 @@ static int l_parse(lua::State* L) { static int l_parse_vcd(lua::State* L) { auto string = lua::require_string(L, 1); auto rootTag = lua::tostring(L, 2); - auto document = xml::parse_vcm("[string]", string, rootTag ? rootTag : "root"); + auto document = xml::parse_vcm( + "[string]", string, rootTag ? rootTag : DEFAULT_ROOT_TAG + ); return push_xml(L, *document->getRoot()); } From 13984539f580b12d189960463b9c890d7cecef3a Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 22 Jul 2026 22:38:43 +0300 Subject: [PATCH 066/217] update doc/*/scripting/filesystem.md --- doc/en/scripting/filesystem.md | 53 ++++++++++++++++++++++++++++++++++ doc/ru/scripting/filesystem.md | 53 ++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/doc/en/scripting/filesystem.md b/doc/en/scripting/filesystem.md index 8341e9665..03976a7f8 100644 --- a/doc/en/scripting/filesystem.md +++ b/doc/en/scripting/filesystem.md @@ -52,6 +52,59 @@ yaml.parse(code: str) -> table Parses a YAML string into a table. +## XML Library + +The library contains functions for serializing and deserializing XML: + +Further, XML as a data type denotes a Lua table with the following structure: +- Element tag is accessible by the key `#` +- XML ​​attributes are accessible as key = value pairs +- Inner elements are accessible by index + +The table is used both as a dictionary and an array. + +Example: + +```xml + + + +``` + +Equivalent Lua table: + +```lua +{ + ['#'] = "panel", + size = "400", + color = "0", + interval = "1", + context = "menu", + { + ['#'] = "button", + onclick = 'menu.page="worlds"', + { + "@Worlds" + } + } +} +``` + +Consequently, the button element is accessible as `panel_element[1]`. + +```lua +-- Serializes a tree in XML format +-- multiline - use multiline formatting +xml.tostring(data: XML, multiline: bool = true) -> str + +-- Parses XML +xml.parse(code: str) -> XML + +-- Parses the VCD (VoxelCore Declaration) format used by the VCM format. +-- root_tag - root element tag (the format does not require an explicit root) +xml.parse(code: str, root_tag: str = "root") -> XML +``` + ## *bjson* library The library contains functions for working with the binary data exchange format [vcbjson](../../specs/binary_json_spec.md). diff --git a/doc/ru/scripting/filesystem.md b/doc/ru/scripting/filesystem.md index 0e060d94c..6334b5e90 100644 --- a/doc/ru/scripting/filesystem.md +++ b/doc/ru/scripting/filesystem.md @@ -52,6 +52,59 @@ yaml.parse(code: str) -> table Парсит YAML строку в таблицу. +## Библиотека xml + +Библиотека содержит функции для сериализации и десериализации XML: + +Далее XML как тип данных обозначает Lua-таблицу со следующей структурой: +- Тег элемента доступен по ключу `#` +- XML-аттрибуты доступны как пары ключ = значение +- Вложенные элементы доступны по индексу + +Таблица используется как словарь, так и массив. + +Пример: + +```xml + + + +``` + +Эквивалентная Lua-таблица: + +```lua +{ + ['#'] = "panel", + size = "400", + color = "0", + interval = "1", + context = "menu", + { + ['#'] = "button", + onclick = 'menu.page="worlds"', + { + "@Worlds" + } + } +} +``` + +Соответственно, элемент button доступен как `элемент_panel[1]`. + +```lua +-- Сериализует дерево в формате XML +-- multiline - использовать ли многострочное форматирование +xml.tostring(data: XML, multiline: bool = true) -> str + +-- Парсит XML +xml.parse(code: str) -> XML + +-- Парсит формат VCD (VoxelCore Declaration), используемый форматом VCM. +-- root_tag - тег корневого элемента (формат не требует наличия явного) +xml.parse(code: str, root_tag: str = "root") -> XML +``` + ## Библиотека bjson Библиотека содержит функции для работы с двоичным форматом обмена данными [vcbjson](../../specs/binary_json_spec.md). From 420d0daf28853e1db1a985f9a74ec38f3d978ddc Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 22 Jul 2026 22:48:16 +0300 Subject: [PATCH 067/217] fix: missing font usage produces fatal error --- src/graphics/core/FontMetics.hpp | 4 ++-- src/graphics/ui/elements/Label.cpp | 3 +++ src/graphics/ui/elements/TextBox.cpp | 4 +++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/graphics/core/FontMetics.hpp b/src/graphics/core/FontMetics.hpp index a835a495b..d62a105d9 100644 --- a/src/graphics/core/FontMetics.hpp +++ b/src/graphics/core/FontMetics.hpp @@ -9,8 +9,8 @@ class Font; struct FontMetrics { std::optional> font; - int lineHeight; - int yoffset; + int lineHeight = 0; + int yoffset = 0; int _glyphInterval = 8; ~FontMetrics(); diff --git a/src/graphics/ui/elements/Label.cpp b/src/graphics/ui/elements/Label.cpp index 14cbeea2d..effb79830 100644 --- a/src/graphics/ui/elements/Label.cpp +++ b/src/graphics/ui/elements/Label.cpp @@ -207,6 +207,9 @@ uint Label::getLinesNumber() const { void Label::draw(const DrawContext& pctx, const Assets& assets) { auto batch = pctx.getBatch2D(); auto font = assets.getShared(fontName); + if (font == nullptr) { + return; + } cache.prepare( font, font->getMetrics(), diff --git a/src/graphics/ui/elements/TextBox.cpp b/src/graphics/ui/elements/TextBox.cpp index d04ca5786..f37673fc1 100644 --- a/src/graphics/ui/elements/TextBox.cpp +++ b/src/graphics/ui/elements/TextBox.cpp @@ -353,7 +353,9 @@ void TextBox::draw(const DrawContext& pctx, const Assets& assets) { void TextBox::drawBackground(const DrawContext& pctx, const Assets& assets) { auto font = assets.getShared(label->getFontName()); - rawTextCache.prepare(font, font->getMetrics(), label->getSize().x); + if (font != nullptr) { + rawTextCache.prepare(font, font->getMetrics(), label->getSize().x); + } glm::vec2 pos = calcPos(); From e9221af4074f36c4c6088ccda8a78789839bd424 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 22 Jul 2026 23:29:27 +0300 Subject: [PATCH 068/217] update xml.parse_vcd --- doc/en/scripting/filesystem.md | 5 +++-- doc/ru/scripting/filesystem.md | 5 +++-- src/logic/scripting/lua/libs/libxml.cpp | 14 ++++++++++++-- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/doc/en/scripting/filesystem.md b/doc/en/scripting/filesystem.md index 03976a7f8..12a190daa 100644 --- a/doc/en/scripting/filesystem.md +++ b/doc/en/scripting/filesystem.md @@ -101,8 +101,9 @@ xml.tostring(data: XML, multiline: bool = true) -> str xml.parse(code: str) -> XML -- Parses the VCD (VoxelCore Declaration) format used by the VCM format. --- root_tag - root element tag (the format does not require an explicit root) -xml.parse(code: str, root_tag: str = "root") -> XML +-- root_tag - a root element with the specified tag will be implicitly created, +-- containing all root elements +xml.parse_vcd(code: str, root_tag: str = "root") -> XML ``` ## *bjson* library diff --git a/doc/ru/scripting/filesystem.md b/doc/ru/scripting/filesystem.md index 6334b5e90..a3bee78a6 100644 --- a/doc/ru/scripting/filesystem.md +++ b/doc/ru/scripting/filesystem.md @@ -101,8 +101,9 @@ xml.tostring(data: XML, multiline: bool = true) -> str xml.parse(code: str) -> XML -- Парсит формат VCD (VoxelCore Declaration), используемый форматом VCM. --- root_tag - тег корневого элемента (формат не требует наличия явного) -xml.parse(code: str, root_tag: str = "root") -> XML +-- root_tag - будет неявно создан корневой элемент с указанным тегом, +-- содержащий все корневые элементы +xml.parse_vcd(code: str, root_tag: str | nil = nil) -> XML ``` ## Библиотека bjson diff --git a/src/logic/scripting/lua/libs/libxml.cpp b/src/logic/scripting/lua/libs/libxml.cpp index ff52b6321..70ccc36bb 100644 --- a/src/logic/scripting/lua/libs/libxml.cpp +++ b/src/logic/scripting/lua/libs/libxml.cpp @@ -82,9 +82,19 @@ static int l_parse_vcd(lua::State* L) { auto string = lua::require_string(L, 1); auto rootTag = lua::tostring(L, 2); auto document = xml::parse_vcm( - "[string]", string, rootTag ? rootTag : DEFAULT_ROOT_TAG + "[string]", string, rootTag ? rootTag : "" ); - return push_xml(L, *document->getRoot()); + const auto& root = *document->getRoot(); + if (rootTag != nullptr) { + return push_xml(L, root); + } + int rootElements = root.size(); + if (rootElements != 1) { + throw std::runtime_error( + "one root element expected, got " + std::to_string(rootElements) + ); + } + return push_xml(L, *root.getElements()[0]); } const luaL_Reg xmllib[] = { From 27bb3015e4793e21f27a40476b832ab0548a2eb7 Mon Sep 17 00:00:00 2001 From: RomanDonw Date: Thu, 23 Jul 2026 17:08:32 +1000 Subject: [PATCH 069/217] improved bytearray --- res/modules/internal/bytearray.lua | 102 ++++++++++++++++++++--------- 1 file changed, 70 insertions(+), 32 deletions(-) diff --git a/res/modules/internal/bytearray.lua b/res/modules/internal/bytearray.lua index a90595979..f7cab06d9 100644 --- a/res/modules/internal/bytearray.lua +++ b/res/modules/internal/bytearray.lua @@ -4,6 +4,7 @@ local FFI = ffi FFI.cdef[[ void* malloc(size_t); + void *realloc(void *, size_t); void free(void*); typedef struct { unsigned char* bytes; @@ -22,12 +23,33 @@ local GC_THRESHOLD = 50 * 1024 * 1024 local function malloc(size) local raw = FFI.C.malloc(size) - if raw == nil then - return nil + if not raw then + error("failed memory allocation of " .. size .. " bytes") end allocated_bytes = allocated_bytes + size + if allocated_bytes >= GC_THRESHOLD then + collectgarbage("step", 200) + allocated_bytes = 0 + end + return raw +end + +local function realloc(buff, size, throwerror) + if throwerror == nil then throwerror = true end + + if size <= 0 then + free(buff) + return + end + + local raw = FFI.C.realloc(buff, size) + if not raw and throwerror then + error("failed memory reallocation of block " .. buff .. " to new size " .. size) + end + + allocated_bytes = allocated_bytes + size if allocated_bytes >= GC_THRESHOLD then collectgarbage("step", 200) allocated_bytes = 0 @@ -38,23 +60,17 @@ end local function grow_buffer(self, elems) local new_capacity = math.ceil(self.capacity / 0.75 + elems) - local prev = self.bytes - self.bytes = malloc(new_capacity) - FFI.copy(self.bytes, prev, self.size) + self.bytes = realloc(self.bytes, new_capacity) self.capacity = new_capacity - free(prev) end local function trim_buffer(self) - if self.size == self.capacity then - return + if self.size == self.capacity then return end + local new_bytes = realloc(self.bytes, self.size, false) + if new_bytes then + self.bytes = new_bytes + self.capacity = self.size end - local size = self.size - local prev = self.bytes - self.bytes = malloc(size) - FFI.copy(self.bytes, prev, self.size) - self.capacity = size - free(prev) end local function count_elements(b) @@ -125,15 +141,16 @@ local function clear(self) self.size = 0 end -local function reserve(self, new_capacity) - if new_capacity <= self.capacity then - return +local function fill(self, byte) + if self.size > 0 then + FFI.fill(self.bytes, self.size, byte) end - local prev = self.bytes - self.bytes = malloc(new_capacity) - FFI.copy(self.bytes, prev, self.size) +end + +local function reserve(self, new_capacity) + if new_capacity <= self.capacity then return end + self.bytes = realloc(self.bytes, new_capacity) self.capacity = new_capacity - free(prev) end local function get_capacity(self) @@ -150,22 +167,43 @@ local function slice(self, offset, length) length = self.size - offset + 1 end local buffer = malloc(length) - if not buffer then - error("malloc(" .. length .. ") returned NULL") - end FFI.copy(buffer, self.bytes + (offset - 1), length) return bytearray_type(buffer, length, length) end +local function copy(self, srcindex, dst, dstindex, size) + if srcindex > 0 and dstindex > 0 and size > 0 and + srcindex + size - 1 <= self.size and + dstindex + size - 1 <= dst.size + then + FFI.copy(dst.bytes + dstindex - 1, self.bytes + srcindex - 1, size) + end +end + +local function move(self, fromindex, toindex, size) + if fromindex > 0 and toindex > 0 and size > 0 and + fromindex + size - 1 <= self.size and + toindex + size - 1 <= self.size + then + local buff = malloc(size) + FFI.copy(buff, self.bytes + fromindex - 1, size) + FFI.copy(self.bytes + toindex - 1, buff, size) + free(buff) + end +end + local bytearray_methods = { - append=append, - insert=insert, - remove=remove, - trim=trim_buffer, - clear=clear, - reserve=reserve, - get_capacity=get_capacity, - slice=slice, + append = append, + insert = insert, + remove = remove, + trim = trim_buffer, + clear = clear, + reserve = reserve, + get_capacity = get_capacity, + slice = slice, + fill = fill, + copy = copy, + move = move } local bytearray_mt = { From 0d641fa82fe3f16773a9cf643f38b12b32ee3caa Mon Sep 17 00:00:00 2001 From: RomanDonw Date: Thu, 23 Jul 2026 17:47:32 +1000 Subject: [PATCH 070/217] (i hope that correctry) optimized bytearray:move method --- res/modules/internal/bytearray.lua | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/res/modules/internal/bytearray.lua b/res/modules/internal/bytearray.lua index f7cab06d9..04212d3e1 100644 --- a/res/modules/internal/bytearray.lua +++ b/res/modules/internal/bytearray.lua @@ -185,10 +185,15 @@ local function move(self, fromindex, toindex, size) fromindex + size - 1 <= self.size and toindex + size - 1 <= self.size then - local buff = malloc(size) - FFI.copy(buff, self.bytes + fromindex - 1, size) - FFI.copy(self.bytes + toindex - 1, buff, size) - free(buff) + if toindex >= fromindex and toindex < fromindex + size then + local buff = malloc(size) + FFI.copy(buff, self.bytes + fromindex - 1, size) + FFI.copy(self.bytes + toindex - 1, buff, size) + free(buff) + else + -- убери(-те) это ветвление, если FFI будет производить копирование не побайтово, и оставь(-те) только верхний блок без условия вообще. + FFI.copy(self.bytes + toindex - 1, self.bytes + fromindex - 1, size) + end end end From 67da57d8708375b05865b714db8e1e560b6546c3 Mon Sep 17 00:00:00 2001 From: RomanDonw Date: Thu, 23 Jul 2026 19:29:19 +1000 Subject: [PATCH 071/217] upgraded/improved LuaCanvas::clear method --- .../scripting/lua/usertypes/lua_type_canvas.cpp | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/logic/scripting/lua/usertypes/lua_type_canvas.cpp b/src/logic/scripting/lua/usertypes/lua_type_canvas.cpp index c670e23df..d03b4e855 100644 --- a/src/logic/scripting/lua/usertypes/lua_type_canvas.cpp +++ b/src/logic/scripting/lua/usertypes/lua_type_canvas.cpp @@ -161,18 +161,11 @@ static LuaCanvas& require_canvas(State* L, int idx) { static int l_clear(State* L) { auto& canvas = require_canvas(L, 1); auto& image = canvas.getData(); - ubyte* data = image.getData(); - RGBA rgba {}; - if (gettop(L) == 1) { - std::fill(data, data + image.getDataSize(), 0); - return 0; - } - rgba = get_rgba(L, 2); - size_t pixels = image.getWidth() * image.getHeight(); - const size_t channels = 4; - for (size_t i = 0; i < pixels * channels; i++) { - data[i] = rgba.arr[i % channels]; - } + size_t pixelscount = image.getWidth() * image.getHeight(); + uint32_t* data = reinterpret_cast(image.getData()); + + if (gettop(L) == 1) std::fill(data, data + pixelscount, 0); + else std::fill(data, data + pixelscount, get_rgba(L, 2).rgba); return 0; } From d3eb7a10bea6825c0ff2da9f459a1b528a6a410f Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 23 Jul 2026 19:29:44 +0300 Subject: [PATCH 072/217] fix texture-faces reading --- src/content/loading/BlockLoader.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/content/loading/BlockLoader.cpp b/src/content/loading/BlockLoader.cpp index f44d3a9ca..56903db79 100644 --- a/src/content/loading/BlockLoader.cpp +++ b/src/content/loading/BlockLoader.cpp @@ -53,6 +53,9 @@ static void load_variant( } } else if (root.has("texture-faces")) { const auto& texarr = root["texture-faces"]; + if (texarr.length() < 6) { + throw std::runtime_error("texture-faces requires 6 elements"); + } for (uint i = 0; i < 6; i++) { variant.textureFaces[i] = texarr[i].asString(); } From 900f118f9483430d2fb0c27c9ceba06485b50b17 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 23 Jul 2026 19:59:31 +0300 Subject: [PATCH 073/217] feat: verbose error messages for parsing functions --- src/coders/commons.cpp | 4 ++++ src/coders/commons.hpp | 2 ++ src/coders/xml.cpp | 18 +++++++++++++++--- src/logic/scripting/lua/libs/libjson.cpp | 9 +++++++-- src/logic/scripting/lua/libs/libtoml.cpp | 9 +++++++-- src/logic/scripting/lua/libs/libxml.cpp | 23 ++++++++++++++++------- src/logic/scripting/lua/libs/libyaml.cpp | 9 +++++++-- 7 files changed, 58 insertions(+), 16 deletions(-) diff --git a/src/coders/commons.cpp b/src/coders/commons.cpp index 273f74f51..aeba0c851 100644 --- a/src/coders/commons.cpp +++ b/src/coders/commons.cpp @@ -57,3 +57,7 @@ std::string parsing_error::errorLog() const { ss << "^"; return ss.str(); } + +std::runtime_error parsing_error::toRuntimeError() const { + return std::runtime_error("parsing error: " + errorLog()); +} diff --git a/src/coders/commons.hpp b/src/coders/commons.hpp index d374cd92b..d1612cfd8 100644 --- a/src/coders/commons.hpp +++ b/src/coders/commons.hpp @@ -73,4 +73,6 @@ public: uint linestart ); std::string errorLog() const; + + std::runtime_error toRuntimeError() const; }; diff --git a/src/coders/xml.cpp b/src/coders/xml.cpp index b40ee7700..a907b49db 100644 --- a/src/coders/xml.cpp +++ b/src/coders/xml.cpp @@ -385,10 +385,22 @@ public: } if (c == '(') { nextChar(); - // TODO: replace with array parsing after moving to dv::value's - std::string value = std::string(readUntil(')')); + int depth = 1; + size_t start = pos; + while (hasNext()) { + char c = nextChar(); + if (c == '(') { + depth++; + } else if (c == ')') { + depth--; + if (depth == 0) { + goBack(1); + break; + } + } + } expect(')'); - return value; + return std::string(source.substr(start, pos - start - 1)); } return std::string(readUntilWhitespace()); } diff --git a/src/logic/scripting/lua/libs/libjson.cpp b/src/logic/scripting/lua/libs/libjson.cpp index a0caa4c33..a4354ea0c 100644 --- a/src/logic/scripting/lua/libs/libjson.cpp +++ b/src/logic/scripting/lua/libs/libjson.cpp @@ -1,4 +1,5 @@ #include "coders/json.hpp" +#include "coders/commons.hpp" #include "api_lua.hpp" static int l_json_stringify(lua::State* L) { @@ -12,8 +13,12 @@ static int l_json_stringify(lua::State* L) { static int l_json_parse(lua::State* L) { auto string = lua::require_string(L, 1); - auto element = json::parse("[string]", string); - return lua::pushvalue(L, element); + try { + auto element = json::parse("[string]", string); + return lua::pushvalue(L, element); + } catch (const parsing_error& err) { + throw err.toRuntimeError(); + } } const luaL_Reg jsonlib[] = { diff --git a/src/logic/scripting/lua/libs/libtoml.cpp b/src/logic/scripting/lua/libs/libtoml.cpp index 4626ee3b9..947496d08 100644 --- a/src/logic/scripting/lua/libs/libtoml.cpp +++ b/src/logic/scripting/lua/libs/libtoml.cpp @@ -1,4 +1,5 @@ #include "coders/toml.hpp" +#include "coders/commons.hpp" #include "api_lua.hpp" using namespace scripting; @@ -16,8 +17,12 @@ static int l_toml_stringify(lua::State* L) { static int l_toml_parse(lua::State* L) { auto string = lua::require_string(L, 1); - auto element = toml::parse("[string]", string); - return lua::pushvalue(L, element); + try { + auto element = toml::parse("[string]", string); + return lua::pushvalue(L, element); + } catch (const parsing_error& err) { + throw err.toRuntimeError(); + } } const luaL_Reg tomllib[] = { diff --git a/src/logic/scripting/lua/libs/libxml.cpp b/src/logic/scripting/lua/libs/libxml.cpp index 70ccc36bb..139e0ee35 100644 --- a/src/logic/scripting/lua/libs/libxml.cpp +++ b/src/logic/scripting/lua/libs/libxml.cpp @@ -1,8 +1,8 @@ #include "coders/xml.hpp" +#include "coders/commons.hpp" #include "api_lua.hpp" static const char* TAG_ATTR = "#"; -static const char* DEFAULT_ROOT_TAG = "root"; static int push_xml(lua::State* L, const xml::xmlelement& elem) { if (elem.isText()) { @@ -73,17 +73,26 @@ static int l_tostring(lua::State* L) { static int l_parse(lua::State* L) { auto string = lua::require_string(L, 1); - auto document = xml::parse("[string]", string); - return push_xml(L, *document->getRoot()); + try { + auto document = xml::parse("[string]", string); + return push_xml(L, *document->getRoot()); + } catch (const parsing_error& err) { + throw err.toRuntimeError(); + } } - static int l_parse_vcd(lua::State* L) { auto string = lua::require_string(L, 1); auto rootTag = lua::tostring(L, 2); - auto document = xml::parse_vcm( - "[string]", string, rootTag ? rootTag : "" - ); + + std::unique_ptr document; + try { + document = xml::parse_vcm( + "[string]", string, rootTag ? rootTag : "" + ); + } catch (const parsing_error& err) { + throw err.toRuntimeError(); + } const auto& root = *document->getRoot(); if (rootTag != nullptr) { return push_xml(L, root); diff --git a/src/logic/scripting/lua/libs/libyaml.cpp b/src/logic/scripting/lua/libs/libyaml.cpp index 42c9906c4..f951727af 100644 --- a/src/logic/scripting/lua/libs/libyaml.cpp +++ b/src/logic/scripting/lua/libs/libyaml.cpp @@ -1,4 +1,5 @@ #include "coders/yaml.hpp" +#include "coders/commons.hpp" #include "api_lua.hpp" static int l_stringify(lua::State* L) { @@ -9,8 +10,12 @@ static int l_stringify(lua::State* L) { static int l_parse(lua::State* L) { auto string = lua::require_string(L, 1); - auto element = yaml::parse("[string]", string); - return lua::pushvalue(L, element); + try { + auto element = yaml::parse("[string]", string); + return lua::pushvalue(L, element); + } catch (const parsing_error& err) { + throw err.toRuntimeError(); + } } const luaL_Reg yamllib[] = { From e0bf82a71e15731aa0a4a6e8670bcf0c9cf392b7 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 23 Jul 2026 20:08:42 +0300 Subject: [PATCH 074/217] add filename info to content unit loading --- src/content/ContentLoader.cpp | 11 ++++++++++- src/io/io.cpp | 15 ++++++++++++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/content/ContentLoader.cpp b/src/content/ContentLoader.cpp index 5120f5c59..14da690a2 100644 --- a/src/content/ContentLoader.cpp +++ b/src/content/ContentLoader.cpp @@ -178,7 +178,16 @@ void ContentUnitLoader::loadUnit( ) { auto folder = pack.folder; auto configFile = folder / (defsDir + "/" + name + ".json"); - if (io::exists(configFile)) loadUnit(def, full, configFile); + if (io::exists(configFile)) { + try { + loadUnit(def, full, configFile); + } catch (const std::runtime_error& err) { + throw std::runtime_error( + "file " + util::quote(configFile.string()) + ": " + + std::string(err.what()) + ); + } + } } void ContentLoader::loadBlockMaterial( diff --git a/src/io/io.cpp b/src/io/io.cpp index 2f4eab11c..638108587 100644 --- a/src/io/io.cpp +++ b/src/io/io.cpp @@ -173,8 +173,12 @@ bool io::write_binary_json( } dv::value io::read_json(const path& filename) { - std::string text = io::read_string(filename); - return json::parse(filename.string(), text); + auto text = io::read_string(filename); + try { + return json::parse(filename.string(), text); + } catch (const parsing_error& err) { + throw err.toRuntimeError(); + } } dv::value io::read_binary_json(const path& file) { @@ -184,7 +188,12 @@ dv::value io::read_binary_json(const path& file) { } dv::value io::read_toml(const path& file) { - return toml::parse(file.string(), io::read_string(file)); + auto text = io::read_string(file); + try { + return toml::parse(file.string(), text); + } catch (const parsing_error& err) { + throw err.toRuntimeError(); + } } std::vector io::read_list(const io::path& filename) { From 7d148ae2cf476368e64b086a987d336288c54675 Mon Sep 17 00:00:00 2001 From: Onran Date: Fri, 24 Jul 2026 03:14:47 +0900 Subject: [PATCH 075/217] all camelCase occurences in io_stream changed to snake_case --- res/modules/io_stream.lua | 212 +++++++++++++++++++------------------- 1 file changed, 108 insertions(+), 104 deletions(-) diff --git a/res/modules/io_stream.lua b/res/modules/io_stream.lua index 98a37529c..522e6ecb7 100644 --- a/res/modules/io_stream.lua +++ b/res/modules/io_stream.lua @@ -25,13 +25,13 @@ local ALL_FLUSH_MODES = { local CR = string.byte('\r') local LF = string.byte('\n') -local function readFully(result, readFunc) +local function read_fully(result, read_func) local isTable = type(result) == "table" local buf repeat - buf = readFunc(MAX_BUFFER_SIZE) + buf = read_func(MAX_BUFFER_SIZE) if isTable then for i = 1, #buf do @@ -59,29 +59,29 @@ ioLib - I/O library. Should include the following functions: close(descriptor: int) --]] -function io_stream.new(descriptor, binaryMode, ioLib, mode, flushMode) +function io_stream.new(descriptor, binary_mode, io_lib, mode, flush_mode) mode = mode or DEFAULT_MODE - flushMode = flushMode or FLUSH_MODE_ALL + flush_mode = flush_mode or FLUSH_MODE_ALL local self = setmetatable({}, io_stream) self.descriptor = descriptor - self.binaryMode = binaryMode - self.maxBufferSize = MAX_BUFFER_SIZE - self.ioLib = ioLib + self.binary_mode = binary_mode + self.max_buffer_size = MAX_BUFFER_SIZE + self.io_lib = io_lib self:set_mode(mode) - self:set_flush_mode(flushMode) + self:set_flush_mode(flush_mode) return self end function io_stream:is_binary_mode() - return self.binaryMode + return self.binary_mode end -function io_stream:set_binary_mode(binaryMode) - self.binaryMode = binaryMode ~= nil +function io_stream:set_binary_mode(binary_mode) + self.binary_mode = binary_mode ~= nil end function io_stream:get_mode() @@ -93,52 +93,52 @@ function io_stream:set_mode(mode) error("invalid stream mode: "..mode) end - if self.writeBuffer then - self.writeBuffer:clear() - self.writeBuffer = nil + if self.write_buffer then + self.write_buffer:clear() + self.write_buffer = nil end - if self.readBuffer then - self.readBuffer:clear() - self.readBuffer = nil + if self.read_buffer then + self.read_buffer:clear() + self.read_buffer = nil end if mode == BUFFERED_MODE then - self.writeBuffer = Bytearray(self.maxBufferSize) - self.readBuffer = Bytearray(self.maxBufferSize) + self.write_buffer = Bytearray(self.max_buffer_size) + self.read_buffer = Bytearray(self.max_buffer_size) end self.mode = mode end function io_stream:get_flush_mode() - return self.flushMode + return self.flush_mode end -function io_stream:set_flush_mode(flushMode) - if not table.has(ALL_FLUSH_MODES, flushMode) then - error("invalid flush mode: "..flushMode) +function io_stream:set_flush_mode(flush_mode) + if not table.has(ALL_FLUSH_MODES, flush_mode) then + error("invalid flush mode: " .. flush_mode) end - self.flushMode = flushMode + self.flush_mode = flush_mode end function io_stream:get_max_buffer_size() - return self.maxBufferSize + return self.max_buffer_size end -function io_stream:set_max_buffer_size(maxBufferSize) - self.maxBufferSize = maxBufferSize +function io_stream:set_max_buffer_size(max_buffer_size) + self.max_buffer_size = max_buffer_size - self.writeBuffer = Bytearray(self.maxBufferSize) - self.readBuffer = Bytearray(self.maxBufferSize) + self.write_buffer = Bytearray(self.max_buffer_size) + self.read_buffer = Bytearray(self.max_buffer_size) end function io_stream:available(length) - local available = self.ioLib.available and self.ioLib.available(self.descriptor) or 0 + local available = self.io_lib.available and self.io_lib.available(self.descriptor) or 0 if self.mode == BUFFERED_MODE then - available = available + #self.readBuffer + available = available + #self.read_buffer end if not length then @@ -148,86 +148,86 @@ function io_stream:available(length) end end -function io_stream:__read(length, fromReadFully) +function io_stream:__read(length, from_read_fully) if self.mode == YIELD_MODE then - if fromReadFully then - return self.ioLib.read(self.descriptor, length) + if from_read_fully then + return self.io_lib.read(self.descriptor, length) end local buffer = Bytearray() while #buffer < length do - buffer:append(self.ioLib.read(self.descriptor, length - #buffer)) + buffer:append(self.io_lib.read(self.descriptor, length - #buffer)) if #buffer < length then coroutine.yield() end end return buffer elseif self.mode == BUFFERED_MODE then - local bufLen = #self.readBuffer + local buf_len = #self.read_buffer if bufLen < length then - self.readBuffer:append( - self.ioLib.read(self.descriptor, self.maxBufferSize - bufLen) + self.read_buffer:append( + self.io_lib.read(self.descriptor, self.max_buffer_size - buf_len) ) end - bufLen = #self.readBuffer + bufLen = #self.read_buffer - length = math.min(bufLen, length) + length = math.min(buf_len, length) - local copy = self.readBuffer:slice(1, length) + local copy = self.read_buffer:slice(1, length) if bufLen == length then - self.readBuffer:clear() + self.read_buffer:clear() else - self.readBuffer:remove(1, length) + self.read_buffer:remove(1, length) end return copy elseif self.mode == DEFAULT_MODE then - return self.ioLib.read(self.descriptor, length) + return self.io_lib.read(self.descriptor, length) end end function io_stream:__write(data) if self.mode == BUFFERED_MODE then - local dataLength = #data + local data_length = #data - if #self.writeBuffer + dataLength > self.maxBufferSize then + if #self.write_buffer + data_length > self.max_buffer_size then self:flush() end - if dataLength > self.maxBufferSize then - local toWrite = math.floor(dataLength / self.maxBufferSize) * self.maxBufferSize - local toSave = dataLength - toWrite + if data_length > self.max_buffer_size then + local to_write = math.floor(data_length / self.max_buffer_size) * self.max_buffer_size + local to_save = data_length - to_write - self.ioLib.write(self.descriptor, data:slice(1, toWrite)) + self.io_lib.write(self.descriptor, data:slice(1, to_write)) self:flush() - self.writeBuffer = data:slice(toWrite + 1, toSave) - else self.writeBuffer:append(data) end + self.write_buffer = data:slice(to_write + 1, to_save) + else self.write_buffer:append(data) end elseif self.mode == DEFAULT_MODE or self.mode == YIELD_MODE then - return self.ioLib.write(self.descriptor, data) + return self.io_lib.write(self.descriptor, data) end end -function io_stream:read_fully(useTable) - if self.binaryMode then - local result = useTable and Bytearray() or { } +function io_stream:read_fully(use_table) + if self.binary_mode then + local result = use_table and { } or Bytearray() local avail = self:available() if avail == 0 then - avail = self.maxBufferSize + avail = self.max_buffer_size end - readFully(result, function() return self:__read(avail, true) end) + read_fully(result, function() return self:__read(avail, true) end) return result else - if useTable then + if use_table then local lines = { } local line @@ -242,7 +242,7 @@ function io_stream:read_fully(useTable) else local result = Bytearray() - readFully(result, function() return self:__read(self.maxBufferSize) end) + read_fully(result, function() return self:__read(self.max_buffer_size) end) return utf8.tostring(result) end @@ -284,57 +284,61 @@ function io_stream:write_line(str) self:__write(utf8.tobytes(str .. "\n")) end -function io_stream:read(arg, useTable) - local argType = type(arg) +function io_stream:read(arg, use_table) + local arg_type = type(arg) - if self.binaryMode then - local byteArr + if self.binary_mode then + local byte_arr - if argType == "number" then + if arg_type == "number" then -- using 'arg' as length - byteArr = self:__read(arg) + byte_arr = self:__read(arg) - if useTable == true then + if use_table == true then local t = { } - for i = 1, #byteArr do - t[i] = byteArr[i] + for i = 1, #byte_arr do + t[i] = byte_arr[i] end return t else - return byteArr + return byte_arr end - elseif argType == "string" then + elseif arg_type == "string" then return byteutil.unpack( arg, self:__read(byteutil.get_size(arg)) ) - elseif argType == nil then + elseif arg_type == "nil" then error( "in binary mode the first argument must be a string data format".. " for the library \"byteutil\" or the number of bytes to read" ) else - error("unknown argument type: "..argType) + error("unknown argument type: "..arg_type) end else if not arg then return self:read_line() else - local linesCount = arg - local trimLastEmptyLines = useTable or true + local lines_count = arg + local trim_last_empty_lines = use_table - if linesCount < 0 then error "count of lines to read must be positive" end + if use_table == nil then + trim_last_empty_lines = true + end + + if lines_count < 0 then error "count of lines to read must be positive" end local result = { } - for i = 1, linesCount do + for i = 1, lines_count do result[i] = self:read_line() end - if trimLastEmptyLines then + if trim_last_empty_lines then local i = #result while i >= 0 do @@ -362,53 +366,53 @@ function io_stream:read(arg, useTable) end function io_stream:write(arg, ...) - local argType = type(arg) + local arg_type = type(arg) - if self.binaryMode then - local byteArr + if self.binary_mode then + local byte_arr - if argType ~= "string" then + if arg_type ~= "string" then -- using arg as bytes table/bytearray - if argType == "table" then - byteArr = Bytearray(arg) + if arg_type == "table" then + byte_arr = Bytearray(arg) else - byteArr = arg + byte_arr = arg end else - byteArr = byteutil.pack(arg, ...) + byte_arr = byteutil.pack(arg, ...) end - self:__write(byteArr) + self:__write(byte_arr) else - if argType == "string" then + if arg_type == "string" then self:write_line(arg) elseif argType == "table" then for i = 1, #arg do self:write_line(arg[i]) end - else error("unknown argument type: "..argType) end + else error("unknown argument type: "..arg_type) end end end function io_stream:seek(mode, offset) - if not self.ioLib.seek then + if not self.io_lib.seek then error("cannot seek this stream") end - self.ioLib.seek(self.descriptor, mode, offset) + self.io_lib.seek(self.descriptor, mode, offset) end function io_stream:tell() - if not self.ioLib.tell then + if not self.io_lib.tell then error("cannot tell this stream") end - return self.ioLib.tell(self.descriptor) + return self.io_lib.tell(self.descriptor) end function io_stream:is_alive() - return self.ioLib.is_alive(self.descriptor) + return self.io_lib.is_alive(self.descriptor) end function io_stream:is_closed() @@ -417,24 +421,24 @@ end function io_stream:close() if self.mode == BUFFERED_MODE then - self.readBuffer:clear() - self.writeBuffer:clear() + self.read_buffer:clear() + self.write_buffer:clear() end - return self.ioLib.close(self.descriptor) + return self.io_lib.close(self.descriptor) end function io_stream:flush() - if self.mode == BUFFERED_MODE and #self.writeBuffer > 0 then - self.ioLib.write(self.descriptor, self.writeBuffer) - self.writeBuffer:clear() + if self.mode == BUFFERED_MODE and #self.write_buffer > 0 then + self.io_lib.write(self.descriptor, self.write_buffer) + self.write_buffer:clear() end - if self.flushMode ~= FLUSH_MODE_ONLY_BUFFER then - if self.ioLib.flush then - self.ioLib.flush(self.descriptor) + if self.flush_mode ~= FLUSH_MODE_ONLY_BUFFER then + if self.io_lib.flush then + self.io_lib.flush(self.descriptor) elseif self:is_closed() then error("stream is closed") end end end -return io_stream +return io_stream \ No newline at end of file From 2bbcb77bcf8dbe953702137b41ba716d36a3e540 Mon Sep 17 00:00:00 2001 From: Onran Date: Fri, 24 Jul 2026 03:29:02 +0900 Subject: [PATCH 076/217] camelCase in some stream providers changed to snake_case --- .../internal/stream_providers/bytearray.lua | 70 +++++++++---------- .../internal/stream_providers/socket.lua | 30 ++++---- 2 files changed, 50 insertions(+), 50 deletions(-) diff --git a/res/modules/internal/stream_providers/bytearray.lua b/res/modules/internal/stream_providers/bytearray.lua index 0f168c851..e377ee784 100644 --- a/res/modules/internal/stream_providers/bytearray.lua +++ b/res/modules/internal/stream_providers/bytearray.lua @@ -5,73 +5,73 @@ local lib = { } local buffers = { } local positions = { } -local nextDescriptor = 0 +local next_descriptor = 0 -local function openDescriptor(buffer) - nextDescriptor = nextDescriptor + 1 +local function open_descriptor(buffer) + next_descriptor = next_descriptor + 1 - buffers[nextDescriptor] = buffer - positions[nextDescriptor] = 1 + buffers[next_descriptor] = buffer + positions[next_descriptor] = 1 - return nextDescriptor + return next_descriptor end -local function requireDescriptor(descriptor) +local function require_descriptor(descriptor) if not buffers[descriptor] then error("unknown descriptor") end end function lib.read(descriptor, length) - requireDescriptor(descriptor) + require_descriptor(descriptor) local buf = buffers[descriptor] - local bufLength = #buf + local buf_length = #buf local pos = positions[descriptor] - local toRead = math.min(bufLength - pos + 1, length) + local to_read = math.min(buf_length - pos + 1, length) - if toRead <= 0 then + if to_read <= 0 then return Bytearray() end - local segment = buf:slice(pos, toRead) + local segment = buf:slice(pos, to_read) - positions[descriptor] = pos + toRead + positions[descriptor] = pos + to_read return segment end function lib.write(descriptor, data) - requireDescriptor(descriptor) + require_descriptor(descriptor) local buf = buffers[descriptor] local pos = positions[descriptor] - local bufLength = #buf - local dataLength = #data + local buf_length = #buf + local data_length = #data - local endPos = pos + dataLength - 1 + local end_pos = pos + data_length - 1 -- size ensuring - if endPos > bufLength then - for i = bufLength + 1, endPos do + if endPos > buf_length then + for i = buf_length + 1, end_pos do buf[i] = 0 end end - for i = 1, dataLength do + for i = 1, data_length do buf[i + pos - 1] = data[i] end - positions[descriptor] = pos + dataLength + positions[descriptor] = pos + data_length end function lib.seek(descriptor, mode, offset) - requireDescriptor(descriptor) + require_descriptor(descriptor) local buf = buffers[descriptor] - local bufLength = #buf + local buf_length = #buf local base @@ -80,26 +80,26 @@ function lib.seek(descriptor, mode, offset) elseif mode == 'c' then base = positions[descriptor] elseif mode == 'e' then - base = bufLength + 1 + base = buf_length + 1 else error('invalid seek mode') end - local newPos = base + offset + local new_pos = base + offset - if newPos < 1 then + if new_pos < 1 then error('failed to seek stream') end - positions[descriptor] = newPos + positions[descriptor] = new_pos end function lib.tell(descriptor) - requireDescriptor(descriptor) + require_descriptor(descriptor) return positions[descriptor] end function lib.available(descriptor) - requireDescriptor(descriptor) + require_descriptor(descriptor) local buf = buffers[descriptor] local pos = positions[descriptor] @@ -112,20 +112,20 @@ function lib.is_alive(descriptor) end function lib.close(descriptor) - requireDescriptor(descriptor) + require_descriptor(descriptor) buffers[descriptor] = nil positions[descriptor] = nil end -return function(buffer, binaryMode) - if binaryMode == nil then - binaryMode = true +return function(buffer, binary_mode) + if binary_mode == nil then + binary_mode = true end return io_stream.new( - openDescriptor(buffer), - binaryMode, + open_descriptor(buffer), + binary_mode, lib ) end \ No newline at end of file diff --git a/res/modules/internal/stream_providers/socket.lua b/res/modules/internal/stream_providers/socket.lua index 0faedd695..c9a728a8b 100644 --- a/res/modules/internal/stream_providers/socket.lua +++ b/res/modules/internal/stream_providers/socket.lua @@ -4,36 +4,36 @@ local lib = { } local sockets = { } -local nextDescriptor = 0 +local next_descriptor = 0 -local function openDescriptor(socket) - nextDescriptor = nextDescriptor + 1 +local function open_descriptor(socket) + next_descriptor = next_descriptor + 1 - sockets[nextDescriptor] = socket + sockets[next_descriptor] = socket - return nextDescriptor + return next_descriptor end -local function requireDescriptor(descriptor) +local function require_descriptor(descriptor) if not sockets[descriptor] then error("unknown descriptor") end end function lib.read(descriptor, length) - requireDescriptor(descriptor) + require_descriptor(descriptor) return sockets[descriptor]:recv(length) end function lib.write(descriptor, data) - requireDescriptor(descriptor) + require_descriptor(descriptor) sockets[descriptor]:send(data) end function lib.available(descriptor) - requireDescriptor(descriptor) + require_descriptor(descriptor) return sockets[descriptor]:available() end @@ -45,20 +45,20 @@ function lib.is_alive(descriptor) end function lib.close(descriptor) - requireDescriptor(descriptor) + require_descriptor(descriptor) sockets[descriptor]:close() sockets[descriptor] = nil end -return function(socket, binaryMode) - if binaryMode == nil then - binaryMode = true +return function(socket, binary_mode) + if binary_mode == nil then + binary_mode = true end return io_stream.new( - openDescriptor(socket), - binaryMode, + open_descriptor(socket), + binary_mode, lib ) end \ No newline at end of file From f539ddf9e0ffd954e46f63793e9ae64182aa3bb3 Mon Sep 17 00:00:00 2001 From: Onran Date: Fri, 24 Jul 2026 03:34:28 +0900 Subject: [PATCH 077/217] docs update --- doc/ru/scripting/builtins/libnetwork.md | 2 +- doc/ru/scripting/io_stream.md | 44 ++++++++++++------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/doc/ru/scripting/builtins/libnetwork.md b/doc/ru/scripting/builtins/libnetwork.md index 5bc5eaafd..46a3eded8 100644 --- a/doc/ru/scripting/builtins/libnetwork.md +++ b/doc/ru/scripting/builtins/libnetwork.md @@ -94,7 +94,7 @@ socket:recv_async( -- Оборачивает сокет в io_stream (см. ../io_stream.md) socket:as_stream( - [опционально] binaryMode: boolean=true + [опционально] binary_mode: boolean=true ) -> io_stream -- Закрывает соединение diff --git a/doc/ru/scripting/io_stream.md b/doc/ru/scripting/io_stream.md index 57f447643..e54ca1fdb 100644 --- a/doc/ru/scripting/io_stream.md +++ b/doc/ru/scripting/io_stream.md @@ -18,7 +18,7 @@ Поток имеет три различных вида режима: - Режим общего поведения (`general` / `mode`) -- Режим сброса (`flush` / `flushMode`) +- Режим сброса (`flush` / `flush_mode`) - Двоичный режим (`binary`) @@ -27,34 +27,34 @@ Определяет, как поток обрабатывает чтение и запись, имеет три подрежима: -| Режим | Описание | -|---------------|--------| -| `"default"` | Прямой режим. `read` может вернуть меньше байт, чем запрошено. `write` сразу отправляет данные в низкоуровневый дескриптор. Нет буферизации. | +| Режим | Описание | +|---------------|-------------------------------------------------------------------------------------------------------------------------------------------------| +| `"default"` | Прямой режим. `read` может вернуть меньше байт, чем запрошено. `write` сразу отправляет данные в низкоуровневый дескриптор. Нет буферизации. | | `"yield"` | Как `default`, но при нехватке данных в `read(n)` поток будет вызывать `coroutine.yield()`, пока не соберёт ровно `n` байт. Удобно для корутин. | -| `"buffered"` | Включает внутренние буферы чтения и записи. `read` берёт данные из буфера, `write` — складывает в буфер. При превышении `maxBufferSize` — ошибка `buffer overflow`. | +| `"buffered"` | Включает внутренние буферы чтения и записи. `read` берёт данные из буфера , `write` — складывает в буфер. | ### flush Работает только в режиме `"buffered"`, имеет два подрежима: -| Режим | Что делает `flush()` | -|------------------|----------------------| -| `"all"` (по умолчанию) | Сначала сбрасывает буфер записи → низкоуровневый `write`, затем вызывает `ioLib.flush(descriptor)` | -| `"buffer"` | Сбрасывает только буфер записи, без вызова системного `flush` | +| Режим | Что делает `flush()` | +|------------------------|-----------------------------------------------------------------------------------------------------| +| `"all"` (по умолчанию) | Сначала сбрасывает буфер записи → низкоуровневый `write`, затем вызывает `io_lib.flush(descriptor)` | +| `"buffer"` | Сбрасывает только буфер записи, без вызова системного `flush` | ### binary **Независимый флаг** (включается через `set_binary_mode(true)` или при создании потока). Определяет, в каком виде методы `read` и `write` принимают и возвращают данные: -| binary = true | binary = false (по умолчанию) | +| binary = true | binary = false (по умолчанию) | |------------------------------------------------------------|----------------------------------------| | Данные — это **байты** (`Bytearray`, таблица чисел 0..255) | Данные — это **текстовые строки** | | `read(n)` → `Bytearray` или `table` | `read()` → одна строка | -| `read("i4 f")` → распаковка через `byteutil.unpack` | `read(n)` → n строк в таблице | +| `read("i4 f")` → распаковка через `byteutil.unpack` | `read(n)` → n строк в таблице | | `write(Bytearray)` → запись байтов | `write("hello")` → строка + `\n` | -| `write("i4", 42)` → `byteutil.pack` | `write({"a","b"})` → две строки с `\n`| +| `write("i4", 42)` → `byteutil.pack` | `write({"a","b"})` → две строки с `\n` | `read_line` / `write_line` — работают как в текстовом режиме @@ -90,17 +90,17 @@ io_stream:set_flush_mode(string) --[[ Читает данные из потока В двоичном режиме: - Если arg - int, то читает из потока arg байт и возвращает ввиде Bytearray или таблицы, если useTable = true + Если arg - int, то читает из потока arg байт и возвращает ввиде Bytearray или таблицы, если use_table = true Если arg - string, то функция интерпретирует arg как шаблон для byteutil. Прочитает кол-во байт, которое определено шаблоном, передаст их в byteutil.unpack и вернёт результат В текстовом режиме: - Если arg - int, то читает нужное кол-во строк с окончанием CRLF/LF из arg и возвращает ввиде таблицы. Также, если trimEmptyLines = true, то удаляет пустые строки с начала и конца из итоговой таблицы + Если arg - int, то читает нужное кол-во строк с окончанием CRLF/LF из arg и возвращает ввиде таблицы. Также, если trim_empty_lines = true, то удаляет пустые строки с начала и конца из итоговой таблицы Если arg не определён, то читает одну строку с окончанием CRLF/LF и возвращает её. --]] io_stream:read( [опционально] arg: int | string, - [опционально] useTable | trimEmptyLines: boolean + [опционально] use_table = false | trim_empty_lines = true: boolean ) -> Bytearray | table | string | table | ... --[[ @@ -128,14 +128,14 @@ io_stream:write_line(string) --[[ В двоичном режиме: - Читает все доступные байты из потока и возвращает ввиде Bytearray или table, если useTable = true + Читает все доступные байты из потока и возвращает ввиде Bytearray или table, если use_table = true В текстовом режиме: - Читает все доступные строки из потока в table если useTable = true, или в одну строку вместе с окончаниями, если нет + Читает все доступные строки из потока в table если use_table = true, или в одну строку вместе с окончаниями, если нет --]] io_stream:read_fully( - [опционально] useTable: boolean + [опционально] use_table: boolean ) -> Bytearray | table | table | string --[[ @@ -203,15 +203,15 @@ io_stream.wrap_bytearray( -- по-умолчанию равен true, поскольку функция из вводных аргументов -- будет использоваться преимущественно для работы с двоичными данными. - [опционально] binaryMode: boolean = true + [опционально] binary_mode: boolean = true ) -> io_stream -- Создаёт новый поток с переданным дескриптором и использующим переданную I/O библиотеку. (Более подробно в core:io_stream.lua) io_stream.new( descriptor: int, - binaryMode: boolean, - ioLib: table, + binary_mode: boolean, + io_lib: table, [опционально] mode: string = "default", - [опционально] flushMode: string = "all" + [опционально] flush_mode: string = "all" ) -> io_stream ``` \ No newline at end of file From 19237b60dce3737cb9aef4bbc5ae8eccd767cd40 Mon Sep 17 00:00:00 2001 From: Onran Date: Fri, 24 Jul 2026 04:01:33 +0900 Subject: [PATCH 078/217] fixes --- res/modules/internal/stream_providers/bytearray.lua | 2 +- res/modules/io_stream.lua | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/res/modules/internal/stream_providers/bytearray.lua b/res/modules/internal/stream_providers/bytearray.lua index e377ee784..a10b11cdf 100644 --- a/res/modules/internal/stream_providers/bytearray.lua +++ b/res/modules/internal/stream_providers/bytearray.lua @@ -54,7 +54,7 @@ function lib.write(descriptor, data) local end_pos = pos + data_length - 1 -- size ensuring - if endPos > buf_length then + if end_pos > buf_length then for i = buf_length + 1, end_pos do buf[i] = 0 end diff --git a/res/modules/io_stream.lua b/res/modules/io_stream.lua index 522e6ecb7..8776ca810 100644 --- a/res/modules/io_stream.lua +++ b/res/modules/io_stream.lua @@ -166,19 +166,19 @@ function io_stream:__read(length, from_read_fully) elseif self.mode == BUFFERED_MODE then local buf_len = #self.read_buffer - if bufLen < length then + if buf_len < length then self.read_buffer:append( self.io_lib.read(self.descriptor, self.max_buffer_size - buf_len) ) end - bufLen = #self.read_buffer + buf_len = #self.read_buffer length = math.min(buf_len, length) local copy = self.read_buffer:slice(1, length) - if bufLen == length then + if buf_len == length then self.read_buffer:clear() else self.read_buffer:remove(1, length) @@ -387,7 +387,7 @@ function io_stream:write(arg, ...) else if arg_type == "string" then self:write_line(arg) - elseif argType == "table" then + elseif arg_type == "table" then for i = 1, #arg do self:write_line(arg[i]) end From 86fdcb289b1cfa510c861c677731599b4f0f0519 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 24 Jul 2026 19:19:07 +0300 Subject: [PATCH 079/217] fix the margin --- res/layouts/pages/new_world.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/layouts/pages/new_world.xml b/res/layouts/pages/new_world.xml index 84a9519fa..fc13d5084 100644 --- a/res/layouts/pages/new_world.xml +++ b/res/layouts/pages/new_world.xml @@ -11,7 +11,7 @@ @World generator From 6910a7e029bd039415cd28a85ee13167e5806ad3 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 24 Jul 2026 19:57:40 +0300 Subject: [PATCH 080/217] add math.noise --- res/modules/internal/extensions/math.lua | 32 ++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/res/modules/internal/extensions/math.lua b/res/modules/internal/extensions/math.lua index df0eadf72..1d168a377 100644 --- a/res/modules/internal/extensions/math.lua +++ b/res/modules/internal/extensions/math.lua @@ -39,3 +39,35 @@ end function math.sign(x) return (x > 0) and 1 or (x < 0 and -1 or 0) end + +local noise_period = 3021 +local noise_rands = {} + +for i=1,noise_period do + table.insert(noise_rands, math.random() * 2.0 - 1.0) +end + +local function smoothstep(t) + return t * t * (3 - 2 * t) +end + +local function sample_noise(x) + x = x % noise_period + local left_cell = math.floor(x) + local right_cell = (left_cell + 1) % noise_period + local t = smoothstep(x - left_cell) + return noise_rands[left_cell + 1] * (1.0 - t) + + noise_rands[right_cell + 1] * t +end + +function math.noise(x, octaves) + octaves = octaves or 1 + local value = 0.0 + local mul = 1.0 + for i=1,octaves do + value = value * (1.0 - mul) + sample_noise(x + 37 * i) * mul + x = x * 2.0 + mul = mul * 0.5 + end + return value +end From 192dbd9b6ca85025c4e982dcd45bd24f9a48530d Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 24 Jul 2026 20:01:14 +0300 Subject: [PATCH 081/217] add math.noise2d --- res/modules/internal/extensions/math.lua | 46 ++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/res/modules/internal/extensions/math.lua b/res/modules/internal/extensions/math.lua index 1d168a377..97bbc33a6 100644 --- a/res/modules/internal/extensions/math.lua +++ b/res/modules/internal/extensions/math.lua @@ -71,3 +71,49 @@ function math.noise(x, octaves) end return value end + +local function lerp(a, b, t) + return a + (b - a) * t +end + +local function rand_2d(x, y) + return noise_rands[math.floor(x * 317 + y) % noise_period + 1] +end + +local function sample_noise2d(x, y) + local x0 = math.floor(x) + local y0 = math.floor(y) + local x1 = x0 + 1 + local y1 = y0 + 1 + + local tx = x - x0 + local ty = y - y0 + + local sx = smoothstep(tx) + local sy = smoothstep(ty) + + local c00 = rand_2d(x0, y0) + local c10 = rand_2d(x1, y0) + local c01 = rand_2d(x0, y1) + local c11 = rand_2d(x1, y1) + + local bottom = lerp(c00, c10, sx) + local top = lerp(c01, c11, sx) + + return lerp(bottom, top, sy) +end + + +function math.noise2d(x, y, octaves) + octaves = octaves or 1 + local value = 0.0 + local mul = 1.0 + for i=1,octaves do + value = value * (1.0 - mul) + + sample_noise2d(x + 37 * i, y + 73 * i) * mul + x = x * 2.0 + y = y * 2.0 + mul = mul * 0.5 + end + return value +end From e09eb41cc7120149cb2d479215a7e35ae5d5b346 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 24 Jul 2026 20:27:42 +0300 Subject: [PATCH 082/217] update doc/*/scripting/extensions.md --- doc/ru/scripting/extensions.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/ru/scripting/extensions.md b/doc/ru/scripting/extensions.md index e7b0fe292..f51ffcb07 100644 --- a/doc/ru/scripting/extensions.md +++ b/doc/ru/scripting/extensions.md @@ -158,6 +158,12 @@ math.sum(x: number, ... | t: table) -> number -- Возвращает целое число, указывающее знак числа (-1/0/1) math.sign(x: number) -> int + +-- Одномерный шум с значениями в диапазоне [-1..1]. +math.noise(x: number, [опционально] octaves: int = 1) -> number + +-- Двумерный шум с значениями в диапазоне [-1..1]. +math.noise2d(x: number, y: number [опционально] octaves: int = 1) -> number ``` ## Расширения для bit From 6b090e20c1beeb4a2bfdcc4b8c75c2322a589142 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 24 Jul 2026 20:33:18 +0300 Subject: [PATCH 083/217] ignore negative noise arguments --- res/modules/internal/extensions/math.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/res/modules/internal/extensions/math.lua b/res/modules/internal/extensions/math.lua index 97bbc33a6..f653bf80e 100644 --- a/res/modules/internal/extensions/math.lua +++ b/res/modules/internal/extensions/math.lua @@ -61,6 +61,7 @@ local function sample_noise(x) end function math.noise(x, octaves) + x = math.abs(x) octaves = octaves or 1 local value = 0.0 local mul = 1.0 @@ -81,6 +82,9 @@ local function rand_2d(x, y) end local function sample_noise2d(x, y) + x = math.abs(x) + y = math.abs(y) + local x0 = math.floor(x) local y0 = math.floor(y) local x1 = x0 + 1 From 28385f236c3e3d2ccf020b116f08a8f6d33ec1a3 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 26 Jul 2026 15:27:38 +0300 Subject: [PATCH 084/217] fix user component initialization exception handling & fix fatal error on uninitialized component access --- src/logic/scripting/scripting_entities.cpp | 10 +++++++++- src/objects/Entity.cpp | 3 +++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/logic/scripting/scripting_entities.cpp b/src/logic/scripting/scripting_entities.cpp index ca9ed6db1..2de177562 100644 --- a/src/logic/scripting/scripting_entities.cpp +++ b/src/logic/scripting/scripting_entities.cpp @@ -148,7 +148,12 @@ void scripting::on_entity_spawn( lua::call(L, 1); } for (auto& component : components) { - create_component(L, -1, *component, args, saved); + try { + create_component(L, -1, *component, args, saved); + } catch (const std::runtime_error& err) { + logger.error() << "could not to initialize user component " + << component->name << ": " << err.what(); + } } for (auto& [packid, pack] : content->getPacks()) { @@ -166,6 +171,9 @@ static void process_entity_callback( const std::string& name, std::function args ) { + if (env == nullptr) { + return; + } auto L = lua::get_main_state(); lua::pushenv(L, *env); if (lua::hasfield(L, "__disabled")) { diff --git a/src/objects/Entity.cpp b/src/objects/Entity.cpp index 6629d2981..46664e24e 100644 --- a/src/objects/Entity.cpp +++ b/src/objects/Entity.cpp @@ -71,6 +71,9 @@ dv::value Entity::serialize() const { if (!scripts.components.empty()) { auto& compsMap = root.object("comps"); for (auto& comp : scripts.components) { + if (comp->env == nullptr) { + continue; + } auto data = scripting::get_component_value(comp->env, SAVED_DATA_VARNAME); compsMap[comp->name] = data; From ea0d23831f06d8050b3efed951d12adbb98918e0 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 26 Jul 2026 15:57:41 +0300 Subject: [PATCH 085/217] add required components check --- src/content/Content.hpp | 1 + src/content/ContentLoader.cpp | 15 ++++++++++++++- src/logic/scripting/scripting.cpp | 3 ++- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/content/Content.hpp b/src/content/Content.hpp index dd0e30bf8..27596c4ac 100644 --- a/src/content/Content.hpp +++ b/src/content/Content.hpp @@ -188,6 +188,7 @@ public: ContentUnitDefs generators; std::unique_ptr const drawGroups; ResourceIndicesSet resourceIndices {}; + std::set components {}; Content( std::unique_ptr indices, diff --git a/src/content/ContentLoader.cpp b/src/content/ContentLoader.cpp index 14da690a2..3c45c4438 100644 --- a/src/content/ContentLoader.cpp +++ b/src/content/ContentLoader.cpp @@ -506,6 +506,7 @@ void ContentLoader::loadScripts(Content& content) { scripting::on_scripts_loading(); load_scripts(content, content.blocks); load_scripts(content, content.items); + auto& tmpContent = content; for (const auto& [packid, runtime] : content.getPacks()) { auto env = runtime->getEnvironment(); @@ -516,7 +517,7 @@ void ContentLoader::loadScripts(Content& content) { // Load entity components io::path componentsDir = folder / "scripts/components"; - foreach_file(componentsDir, [&pack, env](const io::path& file) { + foreach_file(componentsDir, [&pack, env, &tmpContent](const io::path& file) { auto name = pack.id + ":" + file.stem(); scripting::load_entity_component( env, @@ -524,9 +525,21 @@ void ContentLoader::loadScripts(Content& content) { file, pack.id + ":scripts/components/" + file.name() ); + tmpContent.components.insert(name); }); } + for (const auto& [eid, def] : content.entities.getDefs()) { + for (const auto& instance : def->components) { + if (content.components.find(instance.component) == content.components.end()) { + throw std::runtime_error( + "component " + instance.component + + " is not available (required by entity " + eid + ")" + ); + } + } + } + scripting::on_content_loaded(); } diff --git a/src/logic/scripting/scripting.cpp b/src/logic/scripting/scripting.cpp index 6954ee6b8..8c039fa32 100644 --- a/src/logic/scripting/scripting.cpp +++ b/src/logic/scripting/scripting.cpp @@ -744,9 +744,10 @@ void scripting::load_entity_component( const io::path& file, const std::string& fileName ) { + logger.info() << "script (component) " << file.string(); + auto L = lua::get_main_state(); std::string src = io::read_string(file); - logger.info() << "script (component) " << file.string(); lua::loadbuffer(L, *env, src, fileName); lua::store_in_registry(L, lua::CHUNKS_TABLE, name); } From 3de2a41092de93b27559c34f5199720a4fe5d989 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 26 Jul 2026 16:39:54 +0300 Subject: [PATCH 086/217] add 3d text 'font' property --- src/graphics/core/Font.cpp | 4 +++- src/graphics/render/TextsRenderer.cpp | 6 +++++- src/presets/NotePreset.cpp | 2 ++ src/presets/NotePreset.hpp | 1 + 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/graphics/core/Font.cpp b/src/graphics/core/Font.cpp index 7aaa67e80..9f1f0a434 100644 --- a/src/graphics/core/Font.cpp +++ b/src/graphics/core/Font.cpp @@ -162,6 +162,8 @@ static inline void draw_text( const FontStylesScheme* styles, size_t styleMapOffset ) { + bool is3d = std::is_same(); + static FontStylesScheme defStyles {{{}}, {0}}; if (styles == nullptr) { @@ -200,7 +202,7 @@ static inline void draw_text( if (charpage == page){ batch.texture(font.getPage(charpage)); draw_glyph( - batch, pos, glm::vec2(x, y - yOffset / static_cast(font.getLineHeight())), c, right, up, interval, style + batch, pos, glm::vec2(x, y - yOffset * (is3d ? -1 : 1) / static_cast(font.getLineHeight())), c, right, up, interval, style ); } else if (charpage > page && charpage < next){ diff --git a/src/graphics/render/TextsRenderer.cpp b/src/graphics/render/TextsRenderer.cpp index ac93d1e43..b5965dcc4 100644 --- a/src/graphics/render/TextsRenderer.cpp +++ b/src/graphics/render/TextsRenderer.cpp @@ -58,7 +58,10 @@ void TextsRenderer::renderNote( } opacity = preset.xrayOpacity; } - auto& font = assets.require(FONT_DEFAULT); + + auto specifiedFont = assets.get(preset.font); + auto& font = + specifiedFont ? *specifiedFont : assets.require(FONT_DEFAULT); glm::vec3 xvec = note.getAxisX(); glm::vec3 yvec = note.getAxisY(); @@ -118,6 +121,7 @@ void TextsRenderer::renderNote( } auto color = preset.color; batch.setColor(glm::vec4(color.r, color.g, color.b, color.a * opacity)); + font.draw( batch, text, diff --git a/src/presets/NotePreset.cpp b/src/presets/NotePreset.cpp index 59865e80a..a1d68994d 100644 --- a/src/presets/NotePreset.cpp +++ b/src/presets/NotePreset.cpp @@ -13,6 +13,7 @@ dv::value NotePreset::serialize() const { {"render_distance", renderDistance}, {"xray_opacity", xrayOpacity}, {"perspective", perspective}, + {"font", font}, }); } @@ -27,4 +28,5 @@ void NotePreset::deserialize(const dv::value& src) { src.at("render_distance").get(renderDistance); src.at("xray_opacity").get(xrayOpacity); src.at("perspective").get(perspective); + src.at("font").get(font); } diff --git a/src/presets/NotePreset.hpp b/src/presets/NotePreset.hpp index fecdaf18d..e8d030c78 100644 --- a/src/presets/NotePreset.hpp +++ b/src/presets/NotePreset.hpp @@ -28,6 +28,7 @@ struct NotePreset : public Serializable { float renderDistance = 32.0f; float xrayOpacity = 0.0f; float perspective = 1.0f; + std::string font {}; dv::value serialize() const override; void deserialize(const dv::value& src) override; From 4837e1de027e738bd59c4ba9c91b654e5d866f9a Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 26 Jul 2026 17:46:48 +0300 Subject: [PATCH 087/217] fix projected 3d text for non-monospace fonts --- src/graphics/core/Font.cpp | 17 +++++++++++++++-- src/graphics/render/TextsRenderer.cpp | 5 ++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/graphics/core/Font.cpp b/src/graphics/core/Font.cpp index 9f1f0a434..4843387e4 100644 --- a/src/graphics/core/Font.cpp +++ b/src/graphics/core/Font.cpp @@ -196,13 +196,26 @@ static inline void draw_text( float advance = baseAdvance; if (auto glyph = font.getGlyph(c)) { yOffset = glyph->yOffset; - advance = glyph->xAdvance; + advance = glyph->xAdvance / + static_cast(font.getLineHeight()) * 2.0f * + baseAdvance; } uint charpage = c >> 8; if (charpage == page){ batch.texture(font.getPage(charpage)); draw_glyph( - batch, pos, glm::vec2(x, y - yOffset * (is3d ? -1 : 1) / static_cast(font.getLineHeight())), c, right, up, interval, style + batch, + pos, + glm::vec2( + x, + y - yOffset * (is3d ? -1 : 1) / + static_cast(font.getLineHeight()) + ), + c, + right, + up, + interval, + style ); } else if (charpage > page && charpage < next){ diff --git a/src/graphics/render/TextsRenderer.cpp b/src/graphics/render/TextsRenderer.cpp index b5965dcc4..8c3afb6e3 100644 --- a/src/graphics/render/TextsRenderer.cpp +++ b/src/graphics/render/TextsRenderer.cpp @@ -79,7 +79,10 @@ void TextsRenderer::renderNote( yvec = camera.up; } float scale = - (1.0f - preset.perspective) * glm::pow(glm::distance(camera.position, pos), 1.0f-preset.perspective); + (1.0f - preset.perspective) * + glm::pow( + glm::distance(camera.position, pos), 1.0f - preset.perspective + ); xvec *= 1.0f + scale; yvec *= 1.0f + scale; } From 135f351374965a4ebffab9221d9fc195157daa82 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 26 Jul 2026 17:54:29 +0300 Subject: [PATCH 088/217] update doc/*/3d-text.md --- doc/en/3d-text.md | 17 +++++++++-------- doc/ru/3d-text.md | 1 + 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/doc/en/3d-text.md b/doc/en/3d-text.md index b0c9fd1f6..be4a2757f 100644 --- a/doc/en/3d-text.md +++ b/doc/en/3d-text.md @@ -4,14 +4,15 @@ The appearance of 3D text, is configured via a table, like [particles](particles.md). All fields are optional. -| Field | Description | Default | -| --------------- | ---------------------------- | ---------------- | -| display | Display format | static_billboard | -| color | Text color | {1, 1, 1, 1} | -| scale | Text scale | 1 | -| render_distance | Text rendering distance | 32 | -| xray_opacity | Visibility through obstacles | 0 | -| perspective | Perspective coefficient | 1 | +| Field | Description | Default | +| --------------- | ---------------------------------------- | ---------------- | +| display | Display format | static_billboard | +| color | Text color | {1, 1, 1, 1} | +| scale | Text scale | 1 | +| render_distance | Text rendering distance | 32 | +| xray_opacity | Visibility through obstacles | 0 | +| perspective | Perspective coefficient | 1 | +| font | Font. If absent, the default one is used | "" | Available display formats: diff --git a/doc/ru/3d-text.md b/doc/ru/3d-text.md index c2d209434..548289701 100644 --- a/doc/ru/3d-text.md +++ b/doc/ru/3d-text.md @@ -12,6 +12,7 @@ | render_distance | Дистанция отрисовки текста | 32 | | xray_opacity | Коэффициент видимости через препятствия (просвечивание) | 0 | | perspective | Коэффициент перспективы | 1 | +| font | Шрифт. При отсутствии используется основной | "" | Доступные форматы отображения: From e0be7a8d8c9f8d54d7af1b6b976fefc941ebba1f Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 26 Jul 2026 21:46:03 +0300 Subject: [PATCH 089/217] fix custom model blocks render (with some conditions) --- src/content/ContentBuilder.cpp | 13 +++++++------ src/frontend/ContentGfxCache.cpp | 9 ++++++++- src/graphics/render/BlocksRenderer.cpp | 1 - 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/content/ContentBuilder.cpp b/src/content/ContentBuilder.cpp index ce77434ed..25029963b 100644 --- a/src/content/ContentBuilder.cpp +++ b/src/content/ContentBuilder.cpp @@ -26,17 +26,18 @@ std::unique_ptr ContentBuilder::build() { def.rt.tags.insert(tags.add(tag)); } + auto isSolid = [](const Variant& variant) { + return variant.model.type == BlockModelType::BLOCK || + variant.culling == CullingMode::OPTIONAL; + }; + if (def.variants) { for (auto& variant : def.variants->variants) { - variant.rt.solid = - variant.model.type == BlockModelType::BLOCK || - def.explictlySolid; + variant.rt.solid = isSolid(variant) || def.explictlySolid; } def.defaults = def.variants->variants.at(0); } else { - def.defaults.rt.solid = - def.defaults.model.type == BlockModelType::BLOCK || - def.explictlySolid; + def.defaults.rt.solid = isSolid(def.defaults) || def.explictlySolid; } const float EPSILON = 0.01f; diff --git a/src/frontend/ContentGfxCache.cpp b/src/frontend/ContentGfxCache.cpp index b8ce69a5f..9cdcdf70e 100644 --- a/src/frontend/ContentGfxCache.cpp +++ b/src/frontend/ContentGfxCache.cpp @@ -30,6 +30,7 @@ void ContentGfxCache::refreshVariant( uint8_t variantIndex, const Atlas& atlas ) { + std::unordered_map optionalTextures; bool denseRender = settings.denseRender.get(); for (uint side = 0; side < 6; side++) { std::string tex = variant.textureFaces[side]; @@ -43,6 +44,7 @@ void ContentGfxCache::refreshVariant( texOpaque = tex; } else if (variant.culling == CullingMode::OPTIONAL && !denseRender) { tex = texOpaque; + optionalTextures[variant.textureFaces[side]] = texOpaque; } size_t index = getRegionIndex(def.rt.id, variantIndex, side, false); sideregions[index] = atlas.get(tex); @@ -56,7 +58,12 @@ void ContentGfxCache::refreshVariant( if (pos == std::string::npos) { continue; } - if (auto region = atlas.getIf(mesh.texture.substr(pos+1))) { + std::string texture = mesh.texture.substr(pos+1); + const auto& found = optionalTextures.find(texture); + if (found != optionalTextures.end()) { + texture = found->second; + } + if (auto region = atlas.getIf(texture)) { for (auto& vertex : mesh.vertices) { vertex.uv = region->apply(vertex.uv); } diff --git a/src/graphics/render/BlocksRenderer.cpp b/src/graphics/render/BlocksRenderer.cpp index 0aae42010..33ecc8e17 100644 --- a/src/graphics/render/BlocksRenderer.cpp +++ b/src/graphics/render/BlocksRenderer.cpp @@ -538,7 +538,6 @@ void BlocksRenderer::render( break; } case BlockModelType::CUSTOM: { - if (!denseRender) blockCustomModel( {x, y, z}, def, From 40e73ca34b14939468f64a5d349a98400e704bbb Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 27 Jul 2026 01:18:36 +0300 Subject: [PATCH 090/217] fix 3d texts antialiasing render issue --- src/graphics/render/WorldRenderer.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/graphics/render/WorldRenderer.cpp b/src/graphics/render/WorldRenderer.cpp index 965f3fcbf..e6e4f67ed 100644 --- a/src/graphics/render/WorldRenderer.cpp +++ b/src/graphics/render/WorldRenderer.cpp @@ -233,8 +233,6 @@ void WorldRenderer::renderOpaque( const EngineSettings& settings, bool hudVisible ) { - texts->render(ctx, camera, settings, hudVisible, false); - float fogFactor = calcFogFactor(); auto& entityShader = assets.require("entity"); @@ -341,6 +339,7 @@ void WorldRenderer::renderFrameClassic( bool hudVisible, PostProcessing& postProcessing ) { + const auto& settings = engine.getSettings(); const auto& worldInfo = level.getWorld().getInfo(); DrawContext ctx = pctx.sub(); @@ -351,6 +350,7 @@ void WorldRenderer::renderFrameClassic( skybox->draw( level.environment, ctx, camera, worldInfo.daytime, weather.clouds() ); + texts->render(ctx, camera, settings, hudVisible, false); if (debug && hudVisible) { renderDebugLines(ctx, camera); @@ -404,6 +404,7 @@ void WorldRenderer::renderFrameAdvanced( skybox->draw( level.environment, ctx, camera, worldInfo.daytime, weather.clouds() ); + texts->render(ctx, camera, settings, hudVisible, false); if (debug && hudVisible) { renderDebugLines(ctx, camera); } From 0e31bdb83faba49112c4b6f536e7b22402aba6f5 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 27 Jul 2026 20:06:40 +0300 Subject: [PATCH 091/217] fix base:drop hitbox size (revert part of previous change) --- res/content/base/scripts/components/drop.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/res/content/base/scripts/components/drop.lua b/res/content/base/scripts/components/drop.lua index 39bd03ceb..335b81d89 100644 --- a/res/content/base/scripts/components/drop.lua +++ b/res/content/base/scripts/components/drop.lua @@ -10,7 +10,7 @@ dropitem = ARGS if dropitem.item then dropitem.id = item.index(dropitem.item) end -if dropitem then +if dropitem then timer = dropitem.pickup_delay or timer end if SAVED_DATA.item then @@ -26,6 +26,7 @@ function on_save() end if vc.is_client() then + local SCALE = 0.3 local scale = {1, 1, 1} local rotation = mat4.rotate({ math.random(), math.random(), math.random() @@ -35,7 +36,7 @@ if vc.is_client() then local matrix = mat4.idt() rig:set_model(0, item.model_name(dropitem.id)) local bodysize = math.min(scale[1], scale[2], scale[3]) - body:set_size({scale[1], bodysize, scale[3]}) + body:set_size(vec3.mul({scale[1], bodysize, scale[3]}, SCALE)) mat4.mul(matrix, rotation, matrix) mat4.scale(matrix, scale, matrix) rig:set_matrix(0, matrix) From 17095011e66be4ccc044dfa2c1b8fd449e2ab0cc Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 27 Jul 2026 21:10:38 +0300 Subject: [PATCH 092/217] update doc/*/entity-properties.md --- doc/en/entity-properties.md | 2 +- doc/ru/entity-properties.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/en/entity-properties.md b/doc/en/entity-properties.md index 8bc4d5d37..4934e4264 100644 --- a/doc/en/entity-properties.md +++ b/doc/en/entity-properties.md @@ -61,7 +61,7 @@ Determines how the physics engine will work with it. ### *blocking* -Determines whether the entity blocks installation of blocks. +If true - the entity blocks the placement of a block that intersects with the entity's hitbox. *In the future will also block other entities movement.* diff --git a/doc/ru/entity-properties.md b/doc/ru/entity-properties.md index de10cfd32..47126a258 100644 --- a/doc/ru/entity-properties.md +++ b/doc/ru/entity-properties.md @@ -61,7 +61,7 @@ ### Блокирование - *blocking* -Определяет блокирует ли сущность установку блоков. +Определяет блокирует ли сущность установку блока, пересекающегося с хитбоксом сущности. *В будущем будет также блокировать движение других сущностей.* From 439d95355d954c06fcb3aab09c9ed928afdd4f8a Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 27 Jul 2026 21:24:40 +0300 Subject: [PATCH 093/217] add 'selectable' entity property --- src/content/loading/EntityLoader.cpp | 1 + src/objects/Entities.cpp | 2 +- src/objects/EntityDef.hpp | 3 +++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/content/loading/EntityLoader.cpp b/src/content/loading/EntityLoader.cpp index 5741b2079..e785fb0fe 100644 --- a/src/content/loading/EntityLoader.cpp +++ b/src/content/loading/EntityLoader.cpp @@ -89,6 +89,7 @@ template<> void ContentUnitLoader::loadUnit( root.at("skeleton-name").get(def.skeletonName); root.at("material").get(def.material); root.at("blocking").get(def.blocking); + root.at("selectable").get(def.selectable); root.at("solid").get(def.solid); root.at("mass").get(def.mass); root.at("elasticity").get(def.elasticity); diff --git a/src/objects/Entities.cpp b/src/objects/Entities.cpp index fafd99fe5..f0e3e5340 100644 --- a/src/objects/Entities.cpp +++ b/src/objects/Entities.cpp @@ -176,7 +176,7 @@ std::optional Entities::rayCast( for (auto [entity, eid, transform, body] : view.each()) { if (eid.uid == settings.ignoredUid || !body.enabled || - (settings.solidEntitiesOnly && !eid.def.solid)) { + (settings.solidEntitiesOnly && !eid.def.solid) || !eid.def.selectable) { continue; } if (settings.entitiesFilter) { diff --git a/src/objects/EntityDef.hpp b/src/objects/EntityDef.hpp index 6ecb1c917..59aed2d7e 100644 --- a/src/objects/EntityDef.hpp +++ b/src/objects/EntityDef.hpp @@ -42,6 +42,9 @@ struct EntityDef { /// @brief Solid entity material material std::string material; + /// @brief Can the entity be interacted with cursor (on_attack, on_use) + bool selectable = true; + /// @brief Does entity prevent blocks setup bool blocking = true; From 35113777de3e9f250e15b37067f3cc4945d8969f Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 27 Jul 2026 21:31:48 +0300 Subject: [PATCH 094/217] add 'nonselect_entities' raycast parameter --- src/logic/scripting/lua/libs/libentity.cpp | 4 ++++ src/objects/Entities.cpp | 3 ++- src/objects/Entities.hpp | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/logic/scripting/lua/libs/libentity.cpp b/src/logic/scripting/lua/libs/libentity.cpp index 110aee123..4434136ba 100644 --- a/src/logic/scripting/lua/libs/libentity.cpp +++ b/src/logic/scripting/lua/libs/libentity.cpp @@ -311,6 +311,10 @@ static int l_world_raycast(lua::State* L) { entityRaycast.entityFilterExcludeMode = lua::toboolean(L, -1); lua::pop(L); } + if (lua::getfield(L, "nonselect_entities", 1)) { + entityRaycast.includeNonSelectable = lua::toboolean(L, -1); + lua::pop(L); + } if (lua::getfield(L, "filter_blocks", 1)) { load_blocks_filter(L, -1, filteredBlocks); blocksRaycast.filter = &filteredBlocks; diff --git a/src/objects/Entities.cpp b/src/objects/Entities.cpp index f0e3e5340..0902ae85a 100644 --- a/src/objects/Entities.cpp +++ b/src/objects/Entities.cpp @@ -176,7 +176,8 @@ std::optional Entities::rayCast( for (auto [entity, eid, transform, body] : view.each()) { if (eid.uid == settings.ignoredUid || !body.enabled || - (settings.solidEntitiesOnly && !eid.def.solid) || !eid.def.selectable) { + (settings.solidEntitiesOnly && !eid.def.solid) || + (!eid.def.selectable && !settings.includeNonSelectable)) { continue; } if (settings.entitiesFilter) { diff --git a/src/objects/Entities.hpp b/src/objects/Entities.hpp index 0d003ffb1..382790b95 100644 --- a/src/objects/Entities.hpp +++ b/src/objects/Entities.hpp @@ -85,6 +85,7 @@ public: bool solidEntitiesOnly = false; bool entityFilterExcludeMode = false; const std::set* entitiesFilter = nullptr; + bool includeNonSelectable = false; }; struct RaycastResult { From 2b5acfde6279526f476ca58e40407bba415779c0 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 27 Jul 2026 21:38:27 +0300 Subject: [PATCH 095/217] update doc/*/libworld.md & update doc/*/entity-properties.md --- doc/en/entity-properties.md | 4 ++++ doc/en/scripting/builtins/libworld.md | 2 ++ doc/ru/entity-properties.md | 6 +++++- doc/ru/scripting/builtins/libworld.md | 2 ++ 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/doc/en/entity-properties.md b/doc/en/entity-properties.md index 4934e4264..b0aee9aea 100644 --- a/doc/en/entity-properties.md +++ b/doc/en/entity-properties.md @@ -67,6 +67,10 @@ If true - the entity blocks the placement of a block that intersects with the en Default value: *true*. +### *selectable* + +If set to `false` the cursor will ignore the entity, passing the ray. + ### *sensors* A sensor is an area attached to a physical body that detects the entry of other bodies into it. diff --git a/doc/en/scripting/builtins/libworld.md b/doc/en/scripting/builtins/libworld.md index 54b44fe15..5ff749cb8 100644 --- a/doc/en/scripting/builtins/libworld.md +++ b/doc/en/scripting/builtins/libworld.md @@ -92,6 +92,8 @@ world.raycast( -- exclusion filtering mode: -- if true, filter_entities defines the list of ignored entity types entities_exclusion: bool = false, + -- include entities with selectable=false + nonselect_blocks: bool = false; [==[ additional parameters (blocks) ]==] -- id of the block types used for filtering diff --git a/doc/ru/entity-properties.md b/doc/ru/entity-properties.md index 47126a258..2f5ca3791 100644 --- a/doc/ru/entity-properties.md +++ b/doc/ru/entity-properties.md @@ -61,12 +61,16 @@ ### Блокирование - *blocking* -Определяет блокирует ли сущность установку блока, пересекающегося с хитбоксом сущности. +Определяет, блокирует ли сущность установку блока, пересекающегося с хитбоксом сущности. *В будущем будет также блокировать движение других сущностей.* Значение по-умолчанию: *true*. +### Выделяемость - *selectable* + +При значении в `false` курсор будет игнорировать cущность, пропуская луч далее. + ### Список сенсоров - *sensors* Сенсор - область пространства, привязанная к физическому телу, детектирующее попадание в него других тел. diff --git a/doc/ru/scripting/builtins/libworld.md b/doc/ru/scripting/builtins/libworld.md index 6b945aa76..52812f295 100644 --- a/doc/ru/scripting/builtins/libworld.md +++ b/doc/ru/scripting/builtins/libworld.md @@ -91,6 +91,8 @@ world.raycast( -- исключающий режим фильтрации: -- при значении (true) filter_entities определяет список игнорируемых entities_exclusion: bool = false, + -- учитывать сущности с selectable=false + nonselect_entities: bool = false; [==[ дополнительные параметры (блоки) ]==] -- id типов блоков используемых для фильтрации From 2f90083ac62db210d08b5893fc0a1e66c9811528 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 27 Jul 2026 22:17:18 +0300 Subject: [PATCH 096/217] fix doc/en/scripting/buiiltins/libworld.md --- doc/en/scripting/builtins/libworld.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/en/scripting/builtins/libworld.md b/doc/en/scripting/builtins/libworld.md index 5ff749cb8..72bfaf8e4 100644 --- a/doc/en/scripting/builtins/libworld.md +++ b/doc/en/scripting/builtins/libworld.md @@ -93,7 +93,7 @@ world.raycast( -- if true, filter_entities defines the list of ignored entity types entities_exclusion: bool = false, -- include entities with selectable=false - nonselect_blocks: bool = false; + nonselect_entities: bool = false; [==[ additional parameters (blocks) ]==] -- id of the block types used for filtering From 8a40bf4fcd1c9696f91b65cea25216db1e942257 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 28 Jul 2026 23:10:47 +0300 Subject: [PATCH 097/217] feat: auto-focus search textbox in content menu --- res/layouts/pages/content.xml.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/res/layouts/pages/content.xml.lua b/res/layouts/pages/content.xml.lua index 2937542f4..cb63a96c7 100644 --- a/res/layouts/pages/content.xml.lua +++ b/res/layouts/pages/content.xml.lua @@ -3,6 +3,7 @@ function on_open(params) mode = params.mode end refresh() + document.search_textbox.focused = true end -- add - packs to be added to the world (after apply) From 383dcde2e77cdb7ca672203c8bb21b5806a6d155 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 28 Jul 2026 23:18:41 +0300 Subject: [PATCH 098/217] move default block material name to defaults.toml --- res/content/base/config/defaults.toml | 1 + src/content/ContentBuilder.cpp | 3 +++ src/voxels/Block.hpp | 4 +--- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/res/content/base/config/defaults.toml b/res/content/base/config/defaults.toml index 650c24d74..d2c5142cc 100644 --- a/res/content/base/config/defaults.toml +++ b/res/content/base/config/defaults.toml @@ -1,3 +1,4 @@ generator = "base:demo" player-entity = "base:player" hand-skeleton = "base:hand" +block-material = "base:stone" diff --git a/src/content/ContentBuilder.cpp b/src/content/ContentBuilder.cpp index 25029963b..97498b497 100644 --- a/src/content/ContentBuilder.cpp +++ b/src/content/ContentBuilder.cpp @@ -39,6 +39,9 @@ std::unique_ptr ContentBuilder::build() { } else { def.defaults.rt.solid = isSolid(def.defaults) || def.explictlySolid; } + if (def.material.empty()) { + defaults.at("block-material").get(def.material); + } const float EPSILON = 0.01f; def.rt.solid = diff --git a/src/voxels/Block.hpp b/src/voxels/Block.hpp index 5ba3bb267..ded51244e 100644 --- a/src/voxels/Block.hpp +++ b/src/voxels/Block.hpp @@ -37,8 +37,6 @@ inline constexpr size_t MAX_USER_BLOCK_FIELDS_SIZE = 240; inline constexpr int BLOCK_MAX_VARIANTS = 16; -inline std::string DEFAULT_MATERIAL = "base:stone"; - struct BlockFuncsSet { bool init : 1; bool update : 1; @@ -206,7 +204,7 @@ public: dv::value properties = nullptr; /// @brief id of used BlockMaterial, may specify non-existing material - std::string material = DEFAULT_MATERIAL; + std::string material; /// @brief Light emission R, G, B, S (sky lights: sun, moon, radioactive /// clouds) From d9631467fc3c8006ebdadedf6f2bd724287591d6 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 28 Jul 2026 23:28:01 +0300 Subject: [PATCH 099/217] update doc/*/block-properties.md --- doc/en/block-properties.md | 14 +++++++++++++- doc/ru/block-properties.md | 7 ++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/doc/en/block-properties.md b/doc/en/block-properties.md index f3137dd60..d98a8cac5 100644 --- a/doc/en/block-properties.md +++ b/doc/en/block-properties.md @@ -1,6 +1,6 @@ # Block properties -## Visual +## Visual & Audio ### *texture* @@ -70,6 +70,18 @@ Rotation profile (set of available block rotations and behaviour of placing bloc - "pane" - panels, doors, signs - "stairs" - "pane" + flipped variants +### *particles* + +Particles are specified as a JSON object. Property names can be found [in the particles section](particles.md). + +When camera is near to the block, the engine will create an emitter that will run +until the block is destroyed or the camera moves away a certain distance. + +### Material - *material* + +Defines the name of the block's material in the format `pack:material_name`, which affects the selection of block interaction sounds. +Material definitions are located in /block_materials. + ## Variants Some properties can vary dynamically, depending on the variant number stored in the user bits of the block. diff --git a/doc/ru/block-properties.md b/doc/ru/block-properties.md index 50bd7f023..e139f635d 100644 --- a/doc/ru/block-properties.md +++ b/doc/ru/block-properties.md @@ -1,6 +1,6 @@ # Свойства блоков -## Вид +## Вид & Звук ### Текстура - *texture* @@ -78,6 +78,11 @@ При приближении к блоку движок создаст эмиттер, который будет работать до разрушения блока или отдаления камеры на некоторое расстояние. +### Материал - *material* + +Определяет имя материала блока в формате `пак:имя_материала`, что влияет на выбор звуков взаимодействия с блоком. +Определения материалов расположены в /block_materials. + ## Варианты Некоторые свойства могут варьироваться динамически, в зависимости от номера варианта, хранимого в пользовательских битах блока. From 81af4b9e2acba5d67b5bacdb343618c686dd9d5f Mon Sep 17 00:00:00 2001 From: Xertis <118364459+Xertis@users.noreply.github.com> Date: Wed, 29 Jul 2026 18:47:07 +0300 Subject: [PATCH 100/217] fixing errors in rules.lua --- res/modules/internal/rules.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/res/modules/internal/rules.lua b/res/modules/internal/rules.lua index a1a5a8c32..4d739cf35 100644 --- a/res/modules/internal/rules.lua +++ b/res/modules/internal/rules.lua @@ -1,4 +1,4 @@ -local rules = {nexid = 1, rules = {}} +local rules = {nextid = 1, rules = {}} function rules.get_rule(name) local rule = rules.rules[name] @@ -27,13 +27,13 @@ end function rules.reset(name) local rule = rules.get_rule(name) - rules.set(rule.default) + rules.set(name, rule.default) end function rules.listen(name, handler) local rule = rules.get_rule(name) - local id = rules.nexid - rules.nextid = rules.nexid + 1 + local id = rules.nextid + rules.nextid = rules.nextid + 1 rule.listeners[utf8.encode(id)] = handler return id end From 513abd332c3256774314bba239b956ef03b3f16e Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 29 Jul 2026 20:02:30 +0300 Subject: [PATCH 101/217] add platform::new_engine_instance subProcess parameter --- src/logic/scripting/lua/libs/libapp.cpp | 3 +- src/util/platform.cpp | 73 ++++++++++++++++++++++++- src/util/platform.hpp | 4 +- 3 files changed, 77 insertions(+), 3 deletions(-) diff --git a/src/logic/scripting/lua/libs/libapp.cpp b/src/logic/scripting/lua/libs/libapp.cpp index 6442fcbad..12758c724 100644 --- a/src/logic/scripting/lua/libs/libapp.cpp +++ b/src/logic/scripting/lua/libs/libapp.cpp @@ -365,7 +365,8 @@ static int l_start_debug_instance(lua::State* L) { platform::new_engine_instance( std::move(args), - outputPath.empty() ? "" : io::resolve(std::string(outputPath)) + outputPath.empty() ? "" : io::resolve(std::string(outputPath)), + true // debug instance may be detached ); return lua::pushinteger(L, port); } diff --git a/src/util/platform.cpp b/src/util/platform.cpp index f113641e5..76ed8a973 100644 --- a/src/util/platform.cpp +++ b/src/util/platform.cpp @@ -18,7 +18,11 @@ #pragma comment(lib, "winmm.lib") #else #include +#include #include +#include +#include +#include #endif namespace platform::internal { @@ -216,7 +220,9 @@ std::filesystem::path platform::get_executable_path() { } void platform::new_engine_instance( - const std::vector& args, std::filesystem::path outputFile + const std::vector& args, + std::filesystem::path outputFile, + bool subProcess ) { auto executable = get_executable_path(); @@ -285,6 +291,37 @@ void platform::new_engine_instance( &si, &pi ); + if (subProcess) { + HANDLE job = CreateJobObjectW(nullptr, nullptr); + + JOBOBJECT_EXTENDED_LIMIT_INFORMATION info{}; + info.BasicLimitInformation.LimitFlags = + JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + + try { + if (!SetInformationJobObject( + job, + JobObjectExtendedLimitInformation, + &info, + sizeof(info))) { + throw std::runtime_error( + "SetInformationJobObject failed with code: " + + std::to_string(GetLastError()) + ); + } + if (!AssignProcessToJobObject(job, pi.hProcess)) { + throw std::runtime_error( + "SetInformationJobObject failed with code: " + + std::to_string(GetLastError()) + ); + } + } catch (const std::exception& err) { + CloseHandle(job); + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + return; + } + } if (success) { CloseHandle(pi.hProcess); CloseHandle(pi.hThread); @@ -295,6 +332,40 @@ void platform::new_engine_instance( ); } #else + + if (subProcess) { + pid_t pid = fork(); + + int fd = open( + outputFile.string().c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644 + ); + + if (pid == 0) { + prctl(PR_SET_PDEATHSIG, SIGTERM); + + if (getppid() == 1) { + _exit(1); + } + + dup2(fd, STDOUT_FILENO); + dup2(fd, STDERR_FILENO); + close(fd); + + std::vector argv(args.size() + 2); + argv[0] = const_cast(executable.c_str()); + for (int i = 0; i < args.size(); i++) { + argv[i + 1] = args[i].c_str(); + } + argv[args.size() + 1] = nullptr; + + execvp(executable.c_str(), const_cast(argv.data())); + + _exit(127); + } else { + close(fd); + } + } + std::stringstream ss; ss << executable; for (int i = 0; i < args.size(); i++) { diff --git a/src/util/platform.hpp b/src/util/platform.hpp index 54c56a0c5..2ff535248 100644 --- a/src/util/platform.hpp +++ b/src/util/platform.hpp @@ -19,7 +19,9 @@ namespace platform { std::filesystem::path get_executable_path(); /// @brief Run a separate engine instance with specified arguments void new_engine_instance( - const std::vector& args, std::filesystem::path outputFile + const std::vector& args, + std::filesystem::path outputFile, + bool subProcess ); /// @brief Open URL in web browser bool open_url(const std::string& url); From 340aac82c341cbb9cd70ea5c1c1536035344a58e Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 29 Jul 2026 20:24:27 +0300 Subject: [PATCH 102/217] fix macos --- src/util/platform.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/util/platform.cpp b/src/util/platform.cpp index 76ed8a973..bc53d288f 100644 --- a/src/util/platform.cpp +++ b/src/util/platform.cpp @@ -18,12 +18,14 @@ #pragma comment(lib, "winmm.lib") #else #include -#include #include #include #include #include #endif +#ifdef __linux__ + #include +#endif namespace platform::internal { std::filesystem::path get_executable_path(); @@ -333,6 +335,8 @@ void platform::new_engine_instance( } #else +// TODO: implement +#ifndef __APPLE__ if (subProcess) { pid_t pid = fork(); @@ -365,6 +369,7 @@ void platform::new_engine_instance( close(fd); } } +#endif // __APPLE_ std::stringstream ss; ss << executable; From 4c18163f8aea02c8b555c47269c8b609df1ac594 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 29 Jul 2026 22:22:54 +0300 Subject: [PATCH 103/217] add implicit io::path constructor from std::string_view --- src/io/path.hpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/io/path.hpp b/src/io/path.hpp index 538431b7c..3ab95300e 100644 --- a/src/io/path.hpp +++ b/src/io/path.hpp @@ -33,6 +33,8 @@ namespace io { } } + path(std::string_view str) : path(std::string(str)) {} + path(const char* str) : path(std::string(str)) {} bool operator==(const std::string& other) const { From 052ae41271124ebca8d3662db050ac78e04cc406 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 29 Jul 2026 23:04:54 +0300 Subject: [PATCH 104/217] fix platform::new_engine_instance --- src/util/platform.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/util/platform.cpp b/src/util/platform.cpp index bc53d288f..88d42d34a 100644 --- a/src/util/platform.cpp +++ b/src/util/platform.cpp @@ -362,11 +362,13 @@ void platform::new_engine_instance( } argv[args.size() + 1] = nullptr; + prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); execvp(executable.c_str(), const_cast(argv.data())); _exit(127); } else { close(fd); + return; } } #endif // __APPLE_ From 7f813d2eb140d61baee1a345cd113630bb631f0a Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 29 Jul 2026 23:06:02 +0300 Subject: [PATCH 105/217] add app.start_background_instance (test) --- src/devtools/Project.hpp | 2 ++ src/engine/Engine.cpp | 1 + src/logic/scripting/lua/libs/libapp.cpp | 25 ++++++++++++++++++++++++- 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/devtools/Project.hpp b/src/devtools/Project.hpp index daeac7ca0..4906b9373 100644 --- a/src/devtools/Project.hpp +++ b/src/devtools/Project.hpp @@ -1,6 +1,7 @@ #pragma once #include "interfaces/Serializable.hpp" +#include "io/path.hpp" #include #include @@ -21,6 +22,7 @@ struct Permissions { struct Project : Serializable { std::string name; std::string title; + io::path path; std::vector basePacks; Permissions permissions; diff --git a/src/engine/Engine.cpp b/src/engine/Engine.cpp index 10f8cee6d..55ed9fec0 100644 --- a/src/engine/Engine.cpp +++ b/src/engine/Engine.cpp @@ -380,6 +380,7 @@ void Engine::loadAssets() { void Engine::loadProject() { io::path projectFile = "project:project.toml"; project = std::make_unique(); + project->path = "project:"; project->deserialize(io::read_object(projectFile)); logger.info() << "loaded project " << util::quote(project->name); } diff --git a/src/logic/scripting/lua/libs/libapp.cpp b/src/logic/scripting/lua/libs/libapp.cpp index 12758c724..771567fd4 100644 --- a/src/logic/scripting/lua/libs/libapp.cpp +++ b/src/logic/scripting/lua/libs/libapp.cpp @@ -366,11 +366,33 @@ static int l_start_debug_instance(lua::State* L) { platform::new_engine_instance( std::move(args), outputPath.empty() ? "" : io::resolve(std::string(outputPath)), - true // debug instance may be detached + false ); return lua::pushinteger(L, port); } +static int l_start_background_instance(lua::State* L) { + auto scriptPath = lua::require_lstring(L, 1); + io::path outputPath = "user:background.log"; // TODO: + const auto& paths = engine->getPaths(); + + std::vector args { + "--headless", + "--res", paths.getResourcesFolder().u8string(), + "--dir", paths.getUserFilesFolder().u8string(), + "--script", io::resolve(scriptPath).u8string(), + }; + args.emplace_back("--project"); + args.emplace_back(io::resolve(engine->getProject().path).u8string()); + + platform::new_engine_instance( + std::move(args), + outputPath.empty() ? "" : io::resolve(outputPath), + true + ); + return 0; +} + const luaL_Reg applib[] = { /// content {"is_content_loaded", lua::wrap}, @@ -403,5 +425,6 @@ const luaL_Reg applib[] = { {"get_version", lua::wrap}, {"create_memory_device", lua::wrap}, {"start_debug_instance", lua::wrap}, + {"start_background_instance", lua::wrap}, {nullptr, nullptr} }; From 2088b5dec1fd82d6d41619107fafd6151e2e1442 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 30 Jul 2026 21:03:58 +0300 Subject: [PATCH 106/217] disable blocks updating caused by blocks.fill command --- res/scripts/stdcmd.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/scripts/stdcmd.lua b/res/scripts/stdcmd.lua index c2b102675..03df9235f 100644 --- a/res/scripts/stdcmd.lua +++ b/res/scripts/stdcmd.lua @@ -140,7 +140,7 @@ console.add_command( for y=y1,y2 do for z=z1,z2 do for x=x1,x2 do - block.set(x, y, z, id) + block.set(x, y, z, id, 0, true) end end end From a5a2bd0c8ac7a1891d45851e5ed7823078c6cc48 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 30 Jul 2026 21:31:33 +0300 Subject: [PATCH 107/217] cleanup a bit --- src/devtools/AppScriptsControl.cpp | 6 +----- src/devtools/AppScriptsControl.hpp | 3 +-- src/engine/Engine.cpp | 2 +- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/devtools/AppScriptsControl.cpp b/src/devtools/AppScriptsControl.cpp index bb1a386a2..f565949aa 100644 --- a/src/devtools/AppScriptsControl.cpp +++ b/src/devtools/AppScriptsControl.cpp @@ -8,11 +8,7 @@ static debug::Logger logger("app-scripts"); -AppScriptsControl::AppScriptsControl( - const CoreParameters& params, const Project& project -) - : project(project) { - +AppScriptsControl::AppScriptsControl(const CoreParameters& params) { io::path scriptFile = params.scriptFile.empty() ? "project:start.lua" diff --git a/src/devtools/AppScriptsControl.hpp b/src/devtools/AppScriptsControl.hpp index 4e44eef60..3531dc2f9 100644 --- a/src/devtools/AppScriptsControl.hpp +++ b/src/devtools/AppScriptsControl.hpp @@ -14,7 +14,7 @@ struct Project; class AppScriptsControl { public: - AppScriptsControl(const CoreParameters& params, const Project& project); + AppScriptsControl(const CoreParameters& params); void tick(); void loadProjectClientScript(); @@ -24,7 +24,6 @@ public: bool isFinished() const; private: - const Project& project; std::unique_ptr clientScript; std::unique_ptr scriptCoroutine; }; diff --git a/src/engine/Engine.cpp b/src/engine/Engine.cpp index 55ed9fec0..0baa102fc 100644 --- a/src/engine/Engine.cpp +++ b/src/engine/Engine.cpp @@ -192,7 +192,7 @@ void Engine::initialize(CoreParameters coreParameters) { audio::set_input_device(name == "auto" ? "" : name); })); - appScripts = std::make_unique(params, *project); + appScripts = std::make_unique(params); if (params.stdinCommands) { cmd::start_stdin_cmd_reader(*this); From 466095efcb9398f400f27baba045643aede6814c Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 30 Jul 2026 23:47:06 +0300 Subject: [PATCH 108/217] add SystemProcess --- src/main.cpp | 1 + src/util/platform.cpp | 100 ++++++++++++++++++++++++++++++++++++++++-- src/util/platform.hpp | 5 ++- 3 files changed, 102 insertions(+), 4 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 77175bd8a..098d72a70 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,6 +10,7 @@ static debug::Logger logger("main"); static void sigterm_handler(int signum) { + logger.info() << "SIGTERM received"; Engine::getInstance().quit(); } diff --git a/src/util/platform.cpp b/src/util/platform.cpp index 88d42d34a..e0b2587b7 100644 --- a/src/util/platform.cpp +++ b/src/util/platform.cpp @@ -25,6 +25,7 @@ #endif #ifdef __linux__ #include + #include #endif namespace platform::internal { @@ -221,7 +222,98 @@ std::filesystem::path platform::get_executable_path() { #endif } -void platform::new_engine_instance( +class SystemProcess final : public Process { +public: +#ifdef __linux__ + SystemProcess(pid_t pid) : pid(pid) { + } + + ~SystemProcess() { + terminate(); + } +#elif defined(_WIN32) + SystemProcess(const PROCESS_INFORMATION& pi) + : processHandle(pi.hProcess), pid(pi.dwProcessId) { + if (pi.hThread != nullptr) { + CloseHandle(pi.hThread); + } + } + + ~SystemProcess() { + terminate(); + } +#endif + SystemProcess(const SystemProcess&) = delete; + SystemProcess(SystemProcess&&) noexcept = default; + SystemProcess& operator=(const SystemProcess&) = delete; + + SystemProcess& operator=(SystemProcess&& other) noexcept { + if (this != &other) { +#ifdef __linux__ + pid = other.pid; +#elif defined(_WIN32) + if (processHandle != nullptr) { + CloseHandle(processHandle); + } + processHandle = other.processHandle; + pid = other.pid; + other.processHandle = nullptr; + other.pid = 0; +#endif + } + return *this; + } + + bool isActive() const override { +#ifdef __linux__ + if (kill(pid, 0) == 0) { + return true; + } + return errno != ESRCH; +#elif defined(_WIN32) + if (processHandle == nullptr) { + return false; + } + DWORD exitCode; + if (GetExitCodeProcess(processHandle, &exitCode)) { + return exitCode == STILL_ACTIVE; + } + return false; +#endif + } + + void update() override {} + + void waitForEnd() override { +#ifdef __linux__ + waitpid(pid, nullptr, 0); +#elif defined(_WIN32) + if (processHandle != nullptr) { + WaitForSingleObject(processHandle, INFINITE); + } +#endif + }; + + void terminate() final override { +#ifdef __linux__ + kill(pid, SIGKILL); +#elif defined(_WIN32) + if (processHandle != nullptr) { + TerminateProcess(processHandle, 1); + WaitForSingleObject(processHandle, INFINITE); + } +#endif + } +private: +#ifdef __linux__ + pid_t pid; +#elif defined(_WIN32) + HANDLE processHandle = nullptr; + DWORD pid = 0; +#endif +}; + +std::unique_ptr platform::new_engine_instance( const std::vector& args, std::filesystem::path outputFile, bool subProcess @@ -317,11 +409,12 @@ void platform::new_engine_instance( std::to_string(GetLastError()) ); } + return std::make_unique(pi); } catch (const std::exception& err) { CloseHandle(job); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); - return; + return nullptr; } } if (success) { @@ -368,7 +461,7 @@ void platform::new_engine_instance( _exit(127); } else { close(fd); - return; + return std::make_unique(pid); } } #endif // __APPLE_ @@ -393,6 +486,7 @@ void platform::new_engine_instance( ); } #endif + return nullptr; } bool platform::stdin_has_data() { diff --git a/src/util/platform.hpp b/src/util/platform.hpp index 2ff535248..18dc052b6 100644 --- a/src/util/platform.hpp +++ b/src/util/platform.hpp @@ -2,8 +2,11 @@ #include #include +#include #include +#include "interfaces/Process.hpp" + namespace platform { void configure_encoding(); /// @brief Get Environment locale in ISO format ll_CC @@ -18,7 +21,7 @@ namespace platform { /// @brief Get current process running executable path std::filesystem::path get_executable_path(); /// @brief Run a separate engine instance with specified arguments - void new_engine_instance( + std::unique_ptr new_engine_instance( const std::vector& args, std::filesystem::path outputFile, bool subProcess From 4a53090610dce86b8f54d59b438a19395e6039be Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 31 Jul 2026 00:06:25 +0300 Subject: [PATCH 109/217] feat: app coroutine error handling --- res/scripts/stdlib.lua | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/res/scripts/stdlib.lua b/res/scripts/stdlib.lua index 9793216a2..2869bba1c 100644 --- a/res/scripts/stdlib.lua +++ b/res/scripts/stdlib.lua @@ -140,7 +140,20 @@ local __vc_named_coroutines = {} local __vc_next_coroutine = 1 function __vc_start_coroutine(chunk) - local co = coroutine.create(chunk) + local co = coroutine.create(function() + local _, err = xpcall(chunk, function(msg) + local traceback = debug.get_traceback(0) + local s = string.format("%s:", msg) + for i=1,#traceback - 2 do + local frame = traceback[i] + s = s .. "\n\t"..tb_frame_tostring(frame) + end + return s + end) + if err then + error(err) + end + end) local id = __vc_next_coroutine __vc_next_coroutine = __vc_next_coroutine + 1 __vc_coroutines[id] = co From 2d4b4b6081d97f5c0591a4184aa221b5a203e65a Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 31 Jul 2026 18:06:22 +0300 Subject: [PATCH 110/217] update doc/*/rigging.md --- doc/en/rigging.md | 2 +- doc/ru/rigging.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/en/rigging.md b/doc/en/rigging.md index 4b69b5769..abb5ad81a 100644 --- a/doc/en/rigging.md +++ b/doc/en/rigging.md @@ -38,7 +38,7 @@ Models are loaded automatically; adding them to preload.json is not required. ## Models -Models should be located in the models folder. Currently only OBJ format is supported. +Models must be located in the models folder. Currently, the following formats are supported: obj, vec3, and [vcm](vcm.md). >[!IMPORTANT] > When loading an obj model, the \*.mtl file is ignored. diff --git a/doc/ru/rigging.md b/doc/ru/rigging.md index 86ec0a09b..388b19ea2 100644 --- a/doc/ru/rigging.md +++ b/doc/ru/rigging.md @@ -38,7 +38,7 @@ ## Модели -Модели должны располагаться в папке models. На данный момент поддерживается только OBJ формат. +Модели должны располагаться в папке models. На данный момент поддерживаются форматы: obj, vec3 и [vcm](vcm.md). >[!IMPORTANT] > При загрузке obj модели игнорируется файл \*.mtl. From 0701f160e61d5f1eeb9984ea6eebc53ddc09ba27 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 31 Jul 2026 18:07:43 +0300 Subject: [PATCH 111/217] update doc/en/scripting/builtins/libblock.md --- doc/en/scripting/builtins/libblock.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/en/scripting/builtins/libblock.md b/doc/en/scripting/builtins/libblock.md index 4513d57cf..cc69e47d8 100644 --- a/doc/en/scripting/builtins/libblock.md +++ b/doc/en/scripting/builtins/libblock.md @@ -28,7 +28,8 @@ block.get(x: int, y: int, z: int) -> int block.get_states(x: int, y: int, z: int) -> int -- Set block with given integer ID and state (default - 0) at given position. -block.set(x: int, y: int, z: int, id: int, states: int) +-- If noupdate=true is passed, the `on_update` event will not be called for adjacent blocks. +block.set(x: int, y: int, z: int, id: int, states: int, noupdate: boolean=false) -- Places a block with a given integer id and state (default - 0) at given position. -- on behalf of the player, calling the on_placed event. From ee9b2ab2660390056a66d11946abd6ecf70528f2 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 31 Jul 2026 18:57:43 +0300 Subject: [PATCH 112/217] add quit signal check to the client mainloop --- src/engine/Mainloop.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/engine/Mainloop.cpp b/src/engine/Mainloop.cpp index 3a196f522..aa505a9ba 100644 --- a/src/engine/Mainloop.cpp +++ b/src/engine/Mainloop.cpp @@ -43,7 +43,7 @@ void Mainloop::run() { auto& appScripts = engine.getAppScripts(); logger.info() << "main loop started"; - while (!window.isShouldClose()) { + while (!window.isShouldClose() && !engine.isQuitSignal()) { time.update(window.time()); engine.applicationTick(); engine.updateFrontend(); From 5a8fd3ceab614eab3922d2fd133dbc20248f17bf Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sat, 1 Aug 2026 22:32:49 +0300 Subject: [PATCH 113/217] update sub-instance lifetime --- src/engine/Engine.cpp | 1 + src/logic/scripting/lua/libs/libapp.cpp | 8 ++++++-- src/main.cpp | 2 +- src/util/platform.cpp | 6 ++++-- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/engine/Engine.cpp b/src/engine/Engine.cpp index 0baa102fc..f0e20f922 100644 --- a/src/engine/Engine.cpp +++ b/src/engine/Engine.cpp @@ -413,6 +413,7 @@ void Engine::onWorldClosed() { } void Engine::quit() { + logger.info() << "quitSignal set to true"; quitSignal = true; if (!isHeadless()) { window->setShouldClose(true); diff --git a/src/logic/scripting/lua/libs/libapp.cpp b/src/logic/scripting/lua/libs/libapp.cpp index 771567fd4..4d932c272 100644 --- a/src/logic/scripting/lua/libs/libapp.cpp +++ b/src/logic/scripting/lua/libs/libapp.cpp @@ -22,6 +22,10 @@ using namespace scripting; +namespace { + static std::unique_ptr sub_instance = nullptr; +} + /// @brief Check if content is loaded static int l_is_content_loaded(lua::State* L) { return lua::pushboolean(L, content != nullptr); @@ -377,7 +381,7 @@ static int l_start_background_instance(lua::State* L) { const auto& paths = engine->getPaths(); std::vector args { - "--headless", + // "--headless", "--res", paths.getResourcesFolder().u8string(), "--dir", paths.getUserFilesFolder().u8string(), "--script", io::resolve(scriptPath).u8string(), @@ -385,7 +389,7 @@ static int l_start_background_instance(lua::State* L) { args.emplace_back("--project"); args.emplace_back(io::resolve(engine->getProject().path).u8string()); - platform::new_engine_instance( + ::sub_instance = platform::new_engine_instance( std::move(args), outputPath.empty() ? "" : io::resolve(outputPath), true diff --git a/src/main.cpp b/src/main.cpp index 098d72a70..2438d5f50 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,7 +10,7 @@ static debug::Logger logger("main"); static void sigterm_handler(int signum) { - logger.info() << "SIGTERM received"; + logger.info() << "SIGTERM received " << signum; Engine::getInstance().quit(); } diff --git a/src/util/platform.cpp b/src/util/platform.cpp index e0b2587b7..7b08639e9 100644 --- a/src/util/platform.cpp +++ b/src/util/platform.cpp @@ -296,7 +296,8 @@ public: void terminate() final override { #ifdef __linux__ - kill(pid, SIGKILL); + logger.info() << "terminating " << pid; + kill(pid, SIGTERM); #elif defined(_WIN32) if (processHandle != nullptr) { TerminateProcess(processHandle, 1); @@ -461,10 +462,11 @@ std::unique_ptr platform::new_engine_instance( _exit(127); } else { close(fd); + usleep(1000); // waiting for execvp call return std::make_unique(pid); } } -#endif // __APPLE_ +#endif std::stringstream ss; ss << executable; From 4503455ae94082506671355b566e006fd47e0acf Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 2 Aug 2026 01:19:01 +0300 Subject: [PATCH 114/217] add sub-instance project permission & feat: max sub-process depth check --- res/project.toml | 3 ++- src/constants.hpp | 2 ++ src/devtools/Project.hpp | 1 + src/engine/CoreParameters.hpp | 1 + src/logic/scripting/lua/libs/libapp.cpp | 20 ++++++++++++++++++-- src/main.cpp | 2 ++ src/util/command_line.cpp | 13 ++++++++++--- 7 files changed, 36 insertions(+), 6 deletions(-) diff --git a/res/project.toml b/res/project.toml index b641ad00f..281964857 100644 --- a/res/project.toml +++ b/res/project.toml @@ -3,5 +3,6 @@ name = "default" base_packs = ["base"] permissions = [ "network", - "record-audio" + "record-audio", + "sub-instances", ] diff --git a/src/constants.hpp b/src/constants.hpp index f43314f4a..0396f59a6 100644 --- a/src/constants.hpp +++ b/src/constants.hpp @@ -16,6 +16,8 @@ inline constexpr bool ENGINE_DEBUG_BUILD = true; inline const std::string ENGINE_VERSION_STRING = "0.32"; +inline constexpr uint MAX_SUBPROCESS_DEPTH = 2; + /// @brief world regions format version inline constexpr uint REGION_FORMAT_VERSION = 3; diff --git a/src/devtools/Project.hpp b/src/devtools/Project.hpp index 4906b9373..571a8b2c5 100644 --- a/src/devtools/Project.hpp +++ b/src/devtools/Project.hpp @@ -13,6 +13,7 @@ struct Permissions { static inline std::string NETWORK = "network"; static inline std::string RECORD_AUDIO = "record-audio"; static inline std::string WRITE_TO_USER = "write-to-user"; + static inline std::string SUB_INSTANCES = "sub-instances"; std::set permissions; diff --git a/src/engine/CoreParameters.hpp b/src/engine/CoreParameters.hpp index b9f585f47..b5d8074ce 100644 --- a/src/engine/CoreParameters.hpp +++ b/src/engine/CoreParameters.hpp @@ -14,5 +14,6 @@ struct CoreParameters { std::filesystem::path projectFolder; std::string debugServerString; int tps = 20; + int subProcessDepth = 0; std::unordered_map projectArgs; }; diff --git a/src/logic/scripting/lua/libs/libapp.cpp b/src/logic/scripting/lua/libs/libapp.cpp index 4d932c272..e3d41d8c7 100644 --- a/src/logic/scripting/lua/libs/libapp.cpp +++ b/src/logic/scripting/lua/libs/libapp.cpp @@ -341,6 +341,10 @@ static int l_start_debug_instance(lua::State* L) { if (!engine->getProject().permissions.has(Permissions::DEBUGGING)) { throw std::runtime_error("project has no debugging permission"); } + const auto& params = engine->getCoreParameters(); + if (params.subProcessDepth >= MAX_SUBPROCESS_DEPTH) { + throw std::runtime_error("max subprocess depth exceeded"); + } int port = lua::tointeger(L, 1); if (port == 0) { @@ -361,6 +365,8 @@ static int l_start_debug_instance(lua::State* L) { "--res", paths.getResourcesFolder().u8string(), "--dir", paths.getUserFilesFolder().u8string(), "--dbg-server", "tcp:" + std::to_string(port), + "--sub-depth", std::to_string(engine->getCoreParameters() + .subProcessDepth + 1), }; if (!projectPath.empty()) { args.emplace_back("--project"); @@ -376,15 +382,25 @@ static int l_start_debug_instance(lua::State* L) { } static int l_start_background_instance(lua::State* L) { + if (!engine->getProject().permissions.has(Permissions::SUB_INSTANCES)) { + throw std::runtime_error("project has no sub-instances permission"); + } + const auto& params = engine->getCoreParameters(); + if (params.subProcessDepth >= MAX_SUBPROCESS_DEPTH) { + throw std::runtime_error("max subprocess depth exceeded"); + } + auto scriptPath = lua::require_lstring(L, 1); - io::path outputPath = "user:background.log"; // TODO: + io::path outputPath = lua::isstring(L, 2) ? lua::require_lstring(L, 2) : ""; const auto& paths = engine->getPaths(); std::vector args { - // "--headless", + "--headless", "--res", paths.getResourcesFolder().u8string(), "--dir", paths.getUserFilesFolder().u8string(), "--script", io::resolve(scriptPath).u8string(), + "--sub-depth", std::to_string(engine->getCoreParameters() + .subProcessDepth + 1), }; args.emplace_back("--project"); args.emplace_back(io::resolve(engine->getProject().path).u8string()); diff --git a/src/main.cpp b/src/main.cpp index 2438d5f50..83887422a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -26,6 +26,8 @@ int main(int argc, char** argv) { if (!parse_cmdline(argc, argv, coreParameters)) { return EXIT_SUCCESS; } + logger.debug() << "sub-process depth: " + << coreParameters.subProcessDepth; } catch (const std::runtime_error& err) { std::cerr << err.what() << std::endl; return EXIT_FAILURE; diff --git a/src/util/command_line.cpp b/src/util/command_line.cpp index 7f34bcea4..14f49caf4 100644 --- a/src/util/command_line.cpp +++ b/src/util/command_line.cpp @@ -78,12 +78,19 @@ static bool perform_keyword( params.debugServerString = reader.next(); return true; }, "", "open debugging server where is {transport}:{port}"), + ArgC("--sub-depth", [¶ms, &reader]() -> bool { + params.subProcessDepth = reader.nextInt(); + return true; + }, "", "sub-process depth"), ArgC("--help", []() -> bool { std::cout << "VoxelCore v" << ENGINE_VERSION_STRING << "\n\n"; std::cout << "Command-line arguments:\n"; - for (auto& a : argumentsCommandline) { - std::cout << std::setw(24) << std::left << (a.keyword + " " + a.args); - std::cout << "- " << a.help << std::endl; + for (auto& arg : argumentsCommandline) { + if (arg.help.empty()) { + continue; + } + std::cout << std::setw(24) << std::left << (arg.keyword + " " + arg.args); + std::cout << "- " << arg.help << std::endl; } std::cout << std::endl; return false; From 46d3e5c7e1e9fcd511bb6fc4d9077b298763b587 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 2 Aug 2026 01:28:02 +0300 Subject: [PATCH 115/217] cleanup command_line.cpp --- src/util/command_line.cpp | 61 ++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/src/util/command_line.cpp b/src/util/command_line.cpp index 14f49caf4..f05231821 100644 --- a/src/util/command_line.cpp +++ b/src/util/command_line.cpp @@ -13,22 +13,22 @@ namespace fs = std::filesystem; class ArgC { - public: - std::string keyword; - std::function execute; - std::string args; - std::string help; - ArgC( - const std::string& keyword, - std::function execute, - const std::string& args, - const std::string& help - ) { - this->keyword = keyword; - this->execute = execute; - this->args = args; - this->help = help; - } +public: + std::string keyword; + std::function execute; + std::string args; + std::string help; + ArgC( + const std::string& keyword, + std::function execute, + const std::string& args, + const std::string& help + ) { + this->keyword = keyword; + this->execute = execute; + this->args = args; + this->help = help; + } }; @@ -36,60 +36,61 @@ static bool perform_keyword( util::ArgsReader& reader, const std::string& keyword, CoreParameters& params ) { static const std::vector argumentsCommandline = { - ArgC("--res", [¶ms, &reader]() -> bool { + ArgC("--res", [](auto& params, auto& reader) -> bool { params.resFolder = reader.next(); return true; }, "", "set resources directory."), - ArgC("--dir", [¶ms, &reader]() -> bool { + ArgC("--dir", [](auto& params, auto& reader) -> bool { params.userFolder = reader.next(); return true; }, "", "set userfiles directory."), - ArgC("--project", [¶ms, &reader]() -> bool { + ArgC("--project", [](auto& params, auto& reader) -> bool { params.projectFolder = reader.next(); return true; }, "", "set project directory."), - ArgC("--test", [¶ms, &reader]() -> bool { + ArgC("--test", [](auto& params, auto& reader) -> bool { params.testMode = true; params.scriptFile = reader.next(); return true; }, "", "test script file."), - ArgC("--script", [¶ms, &reader]() -> bool { + ArgC("--script", [](auto& params, auto& reader) -> bool { params.testMode = false; params.scriptFile = reader.next(); return true; }, "", "main script file."), - ArgC("--headless", [¶ms]() -> bool { + ArgC("--headless", [](auto& params, auto& reader) -> bool { params.headless = true; return true; }, "", "run in headless mode."), - ArgC("--stdin-cmd", [¶ms]() -> bool { + ArgC("--stdin-cmd", [](auto& params, auto& reader) -> bool { params.stdinCommands = true; return true; }, "", "run commands from stdin."), - ArgC("--tps", [¶ms, &reader]() -> bool { + ArgC("--tps", [](auto& params, auto& reader) -> bool { params.tps = reader.nextInt(); return true; }, "", "headless mode tick-rate (default - 20)."), - ArgC("--version", []() -> bool { + ArgC("--version", [](auto&, auto&) -> bool { std::cout << ENGINE_VERSION_STRING << std::endl; return false; }, "", "display the engine version."), - ArgC("--dbg-server", [¶ms, &reader]() -> bool { + ArgC("--dbg-server", [](auto& params, auto& reader) -> bool { params.debugServerString = reader.next(); return true; }, "", "open debugging server where is {transport}:{port}"), - ArgC("--sub-depth", [¶ms, &reader]() -> bool { + ArgC("--sub-depth", [](auto& params, auto& reader) -> bool { params.subProcessDepth = reader.nextInt(); return true; }, "", "sub-process depth"), - ArgC("--help", []() -> bool { + ArgC("--help", [](auto&, auto&) -> bool { std::cout << "VoxelCore v" << ENGINE_VERSION_STRING << "\n\n"; std::cout << "Command-line arguments:\n"; for (auto& arg : argumentsCommandline) { if (arg.help.empty()) { continue; } - std::cout << std::setw(24) << std::left << (arg.keyword + " " + arg.args); + std::cout << std::setw(24) << std::left + << (arg.keyword + " " + arg.args); std::cout << "- " << arg.help << std::endl; } std::cout << std::endl; @@ -98,7 +99,7 @@ static bool perform_keyword( }; for (auto& a : argumentsCommandline) { if (a.keyword == keyword) { - return a.execute(); + return a.execute(params, reader); } } throw std::runtime_error("unknown argument " + keyword); From 8b2094c6283314cce1f4dcf28b543dff6731efc1 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 2 Aug 2026 15:02:44 +0300 Subject: [PATCH 116/217] add SIGTERM handler --- src/main.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 83887422a..402b829b3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,7 +10,7 @@ static debug::Logger logger("main"); static void sigterm_handler(int signum) { - logger.info() << "SIGTERM received " << signum; + logger.info() << (signum == SIGTERM ? "SIGTERM" : "SIGINT") << " received"; Engine::getInstance().quit(); } @@ -33,7 +33,9 @@ int main(int argc, char** argv) { return EXIT_FAILURE; } std::signal(SIGTERM, sigterm_handler); - +#ifdef NDEBUG + std::signal(SIGINT, sigterm_handler); +#endif debug::Logger::init(coreParameters.userFolder.string() + "/latest.log"); platform::configure_encoding(); From b1e14a294444e4fde541b6d56291635d7f381206 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 2 Aug 2026 16:20:33 +0300 Subject: [PATCH 117/217] add app.terminate(handle) --- src/constants.hpp | 1 + src/logic/scripting/lua/libs/libapp.cpp | 33 ++++++++++++++++++++++--- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/constants.hpp b/src/constants.hpp index 0396f59a6..5bacfe078 100644 --- a/src/constants.hpp +++ b/src/constants.hpp @@ -16,6 +16,7 @@ inline constexpr bool ENGINE_DEBUG_BUILD = true; inline const std::string ENGINE_VERSION_STRING = "0.32"; +inline constexpr uint MAX_SUBPROCESSES = 1; inline constexpr uint MAX_SUBPROCESS_DEPTH = 2; /// @brief world regions format version diff --git a/src/logic/scripting/lua/libs/libapp.cpp b/src/logic/scripting/lua/libs/libapp.cpp index e3d41d8c7..e86b6d88e 100644 --- a/src/logic/scripting/lua/libs/libapp.cpp +++ b/src/logic/scripting/lua/libs/libapp.cpp @@ -20,10 +20,12 @@ #include "window/Window.hpp" #include "world/Level.hpp" +#include + using namespace scripting; namespace { - static std::unique_ptr sub_instance = nullptr; + static std::array, MAX_SUBPROCESSES> processes; } /// @brief Check if content is loaded @@ -405,12 +407,36 @@ static int l_start_background_instance(lua::State* L) { args.emplace_back("--project"); args.emplace_back(io::resolve(engine->getProject().path).u8string()); - ::sub_instance = platform::new_engine_instance( + int handle = -1; + for (int i = 0; i < ::processes.size(); i++) { + if (!::processes[i] || !::processes[i]->isActive()) { + handle = i; + break; + } + } + if (handle == -1) { + throw std::runtime_error("sub-processes limit exceeded"); + } + ::processes[handle] = platform::new_engine_instance( std::move(args), outputPath.empty() ? "" : io::resolve(outputPath), true ); - return 0; + return lua::pushinteger(L, handle); +} + +static int l_terminate(lua::State* L) { + int handle = lua::tointeger(L, 1); + if (handle < 0 || handle >= ::processes.size()) { + throw std::runtime_error("invalid process handle"); + } + auto& process = ::processes[handle]; + if (process == nullptr) { + return lua::pushboolean(L, false); + } + bool active = process->isActive(); + process.reset(); + return lua::pushboolean(L, active); } const luaL_Reg applib[] = { @@ -446,5 +472,6 @@ const luaL_Reg applib[] = { {"create_memory_device", lua::wrap}, {"start_debug_instance", lua::wrap}, {"start_background_instance", lua::wrap}, + {"terminate", lua::wrap}, {nullptr, nullptr} }; From 8de3c0de57018c12473f2fc9c8a0c67dd0aa9f6e Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 2 Aug 2026 17:54:08 +0300 Subject: [PATCH 118/217] update vc library --- res/scripts/stdmin.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/res/scripts/stdmin.lua b/res/scripts/stdmin.lua index 2868dd634..e341f68a1 100644 --- a/res/scripts/stdmin.lua +++ b/res/scripts/stdmin.lua @@ -10,8 +10,9 @@ vc = { is_client = function() return not _vc_headless end, + get_version = __vc_app.get_version, get_setting = __vc_app.get_setting, - get_setting_str = __vc_app.get_setting_str, + str_setting = __vc_app.str_setting, get_setting_info = __vc_app.get_setting_info, } From 2eae8f6f275e7a41ac30309e61f8d22d8a644ac7 Mon Sep 17 00:00:00 2001 From: EsPaKira Date: Sun, 2 Aug 2026 19:18:37 +0400 Subject: [PATCH 119/217] fix block.index() calls in grass_block.lua and export base:util.calc_loot() --- res/content/base/modules/util.lua | 6 +++--- res/content/base/scripts/grass_block.lua | 8 +++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/res/content/base/modules/util.lua b/res/content/base/modules/util.lua index b1a97ad06..1044eb394 100644 --- a/res/content/base/modules/util.lua +++ b/res/content/base/modules/util.lua @@ -12,14 +12,14 @@ function util.drop(ppos, itemid, count, data, pickup_delay) }}) end -local function calc_loot(loot_table) +function calc_loot(loot_table) local results = {} for _, loot in ipairs(loot_table) do local chance = loot.chance or 1 local count = loot.count or 1 - + local roll = math.random() - + if roll < chance then if loot.min and loot.max then count = math.random(loot.min, loot.max) diff --git a/res/content/base/scripts/grass_block.lua b/res/content/base/scripts/grass_block.lua index fb7482c20..b5965de43 100644 --- a/res/content/base/scripts/grass_block.lua +++ b/res/content/base/scripts/grass_block.lua @@ -1,3 +1,6 @@ +local dirtid = block.index("base:dirt") +local grass_blockid = block.index("base:grass_block") + local offsets = {} for lx=-1,1 do for ly=-1,1 do @@ -8,18 +11,17 @@ for lx=-1,1 do end function on_random_update(x, y, z) - local dirtid = block.index('base:dirt'); if block.is_solid_at(x, y+1, z) then block.set(x, y, z, dirtid, 0) return end - local grassblockid = block.index('base:grass_block') + for _, offset in ipairs(offsets) do local nx, ny, nz = x + offset[1], y + offset[2], z + offset[3] if block.get(nx, ny, nz) == dirtid and not block.is_solid_at(nx, ny + 1, nz) then - block.set(nx, ny, nz, grassblockid, 0) + block.set(nx, ny, nz, grass_blockid, 0) return end end From 466f515ea9cf3b23680bc418547a72544ab71f15 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 2 Aug 2026 18:27:19 +0300 Subject: [PATCH 120/217] update doc/*/scripting/builtins/libapp.md --- doc/en/scripting/builtins/libapp.md | 17 +++++++++++++++++ doc/ru/scripting/builtins/libapp.md | 19 ++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/doc/en/scripting/builtins/libapp.md b/doc/en/scripting/builtins/libapp.md index 0ec755689..35084d9bd 100644 --- a/doc/en/scripting/builtins/libapp.md +++ b/doc/en/scripting/builtins/libapp.md @@ -191,3 +191,20 @@ app.reset_content_sources() ``` Resets content sources. + +## Sub-instances + +```lua +-- Creates a headless engine instance with the current project and the specified application script. +-- Returns the instance ID. The number of active sub-instances is currently limited to one. +app.start_background_instance( + -- script file + app_script: string, + -- log file + output_file: string | nil +) -> int + +-- Stops the engine sub-instance. +-- Returns true if the instance was alive at the time of the call. +app.terminate(handle: int) -> boolean +``` diff --git a/doc/ru/scripting/builtins/libapp.md b/doc/ru/scripting/builtins/libapp.md index 07e358ef4..920283ce3 100644 --- a/doc/ru/scripting/builtins/libapp.md +++ b/doc/ru/scripting/builtins/libapp.md @@ -142,4 +142,21 @@ app.set_content_sources(sources: table) -- Сбрасывает список источников контента. app.reset_content_sources() -``` \ No newline at end of file +``` + +## Под-экземпляры + +```lua +-- Создаёт headless-экземпляр движка с текущим проектом и указанным сценарием. +-- Возвращает id экземпляра. Число живых под-экземпляров, на данный момент, ограничено одним. +app.start_background_instance( + -- файл сценария + app_script: string, + -- файл лога + output_file: string | nil +) -> int + +-- Останавливает под-экземпляр движка. +-- Возвращает true если экземпляр был жив в момент вызова. +app.terminate(handle: int) -> boolean +``` From 6bbc78098dfd15b353b2d1d28d37133f4fe3651d Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 2 Aug 2026 18:32:54 +0300 Subject: [PATCH 121/217] add app.is_instance_alive & rename app.terminate to app.terminate_instance --- doc/en/scripting/builtins/libapp.md | 3 +++ doc/ru/scripting/builtins/libapp.md | 5 ++++- src/logic/scripting/lua/libs/libapp.cpp | 14 ++++++++++++-- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/doc/en/scripting/builtins/libapp.md b/doc/en/scripting/builtins/libapp.md index 35084d9bd..61be41d5d 100644 --- a/doc/en/scripting/builtins/libapp.md +++ b/doc/en/scripting/builtins/libapp.md @@ -204,6 +204,9 @@ app.start_background_instance( output_file: string | nil ) -> int +-- Checks if the engine sub-instance is alive. +app.is_instance_alive(handle: int) -> boolean + -- Stops the engine sub-instance. -- Returns true if the instance was alive at the time of the call. app.terminate(handle: int) -> boolean diff --git a/doc/ru/scripting/builtins/libapp.md b/doc/ru/scripting/builtins/libapp.md index 920283ce3..bbdf5cd2f 100644 --- a/doc/ru/scripting/builtins/libapp.md +++ b/doc/ru/scripting/builtins/libapp.md @@ -156,7 +156,10 @@ app.start_background_instance( output_file: string | nil ) -> int +-- Проверяет, жив ли под-экземпляр движка. +app.is_instance_alive(handle: int) -> boolean + -- Останавливает под-экземпляр движка. -- Возвращает true если экземпляр был жив в момент вызова. -app.terminate(handle: int) -> boolean +app.terminate_instance(handle: int) -> boolean ``` diff --git a/src/logic/scripting/lua/libs/libapp.cpp b/src/logic/scripting/lua/libs/libapp.cpp index e86b6d88e..9157b33a6 100644 --- a/src/logic/scripting/lua/libs/libapp.cpp +++ b/src/logic/scripting/lua/libs/libapp.cpp @@ -425,7 +425,16 @@ static int l_start_background_instance(lua::State* L) { return lua::pushinteger(L, handle); } -static int l_terminate(lua::State* L) { +static int l_is_instance_alive(lua::State* L) { + int handle = lua::tointeger(L, 1); + if (handle < 0 || handle >= ::processes.size()) { + throw std::runtime_error("invalid process handle"); + } + auto& process = ::processes[handle]; + return lua::pushboolean(L, process && process->isActive()); +} + +static int l_terminate_instance(lua::State* L) { int handle = lua::tointeger(L, 1); if (handle < 0 || handle >= ::processes.size()) { throw std::runtime_error("invalid process handle"); @@ -472,6 +481,7 @@ const luaL_Reg applib[] = { {"create_memory_device", lua::wrap}, {"start_debug_instance", lua::wrap}, {"start_background_instance", lua::wrap}, - {"terminate", lua::wrap}, + {"is_instance_alive", lua::wrap}, + {"terminate_instance", lua::wrap}, {nullptr, nullptr} }; From 4d010e8320964bc87e927ee5089e690c0250dd5c Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 2 Aug 2026 18:36:12 +0300 Subject: [PATCH 122/217] fix doc/en/scripting/builtins/libapp.md formatting --- doc/en/scripting/builtins/libapp.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/en/scripting/builtins/libapp.md b/doc/en/scripting/builtins/libapp.md index 61be41d5d..48d79bf8e 100644 --- a/doc/en/scripting/builtins/libapp.md +++ b/doc/en/scripting/builtins/libapp.md @@ -180,13 +180,13 @@ app.get_content_sources() -> table Returns a list of content sources (paths), in descending priority order. -``lua +```lua app.set_content_sources(sources: table) ``` Sets a list of content sources (paths). Specified in descending priority order. -``lua +```lua app.reset_content_sources() ``` From 9444dc0c4596bd0c1fd2710488c8d412da8aab1f Mon Sep 17 00:00:00 2001 From: EsPaKira Date: Sun, 2 Aug 2026 20:02:56 +0400 Subject: [PATCH 123/217] fix lua version compare --- res/layouts/pages/content.xml.lua | 44 +++++++++++++++---------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/res/layouts/pages/content.xml.lua b/res/layouts/pages/content.xml.lua index cb63a96c7..70b5a486c 100644 --- a/res/layouts/pages/content.xml.lua +++ b/res/layouts/pages/content.xml.lua @@ -179,7 +179,7 @@ function place_pack(panel, packinfo, callback, position_func) end end -local Version = {}; +local Version = {} function Version.matches_pattern(version) for _, letter in string.gmatch(version, "%.+") do @@ -187,32 +187,32 @@ function Version.matches_pattern(version) return false; end - local t = string.split(version, "."); + local t = string.split(version, ".") return #t == 2 or #t == 3; end end function Version.__equal(ver1, ver2) - return ver1[1] == ver2[1] and ver1[2] == ver2[2] and ver1[3] == ver2[3]; + return ver1[1] == ver2[1] and ver1[2] == ver2[2] and ver1[3] == ver2[3] end function Version.__greater(ver1, ver2) - if ver1[1] ~= ver2[1] then return ver1[1] > ver2[1] end; - if ver1[2] ~= ver2[2] then return ver1[2] > ver2[2] end; - return ver1[3] > ver2[3]; + if ver1[1] ~= ver2[1] then return ver1[1] > ver2[1] end + if ver1[2] ~= ver2[2] then return ver1[2] > ver2[2] end + return ver1[3] > ver2[3] end function Version.__less(ver1, ver2) - return Version.__greater(ver2, ver1); + return Version.__greater(ver2, ver1) end function Version.__greater_or_equal(ver1, ver2) - return not Version.__less(ver1, ver2); + return not Version.__less(ver1, ver2) end function Version.__less_or_equal(ver1, ver2) - return not Version.__greater(ver1, ver2); + return not Version.__greater(ver1, ver2) end Version.operators = { @@ -224,8 +224,8 @@ Version.operators = { } function Version.compare(op, ver1, ver2) - ver1 = string.split(ver1, "."); - ver2 = string.split(ver2, "."); + ver1 = string.split(ver1, ".") + ver2 = string.split(ver2, ".") local comparison_func = Version.operators[op]; @@ -237,14 +237,14 @@ function Version.compare(op, ver1, ver2) end function Version.parse(version) - local op = string.sub(version, 1, 2); + local op = string.sub(version, 1, 2) if op == ">=" or op == "<=" then - return op, string.sub(version, #op + 1); + return op, string.sub(version, #op + 1) end op = string.sub(version, 1, 1); if op == ">" or op == "<" then - return op, string.sub(version, #op + 1); + return op, string.sub(version, #op + 1) end return "=", version; @@ -252,12 +252,12 @@ end local function compare_version(dependent_version, actual_version) if Version.matches_pattern(dependent_version) and Version.matches_pattern(actual_version) then - local op, dep_ver = Version.parse_version(dependent_version); - Version.compare(op, dep_ver, actual_version); + local op, dep_ver = Version.parse_version(dependent_version) + Version.compare(op, dep_ver, actual_version) elseif dependent_version == "*" or dependent_version == actual_version then - return true; + return true else - return false; + return false end end @@ -277,15 +277,15 @@ function check_dependencies(packinfo) ) end - local dep_pack = pack.get_info(depid); - + local dep_pack = pack.get_info(depid) + local op, ver = Version.parse(depver) + if not compare_version(depver, dep_pack.version) then - local op, ver = Version.parse(depver) return string.format( "%s: %s != %s (%s)", gui.str("error.dependency-version-not-met"), dep_pack.version, ver, depid - ); + ) end if table.has(packs_installed, packinfo.id) then From 4be73acc61f6805c9df2743e68a86ee646d43e25 Mon Sep 17 00:00:00 2001 From: EsPaKira Date: Sun, 2 Aug 2026 21:07:02 +0400 Subject: [PATCH 124/217] fix fix lua version comparision --- res/layouts/pages/content.xml.lua | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/res/layouts/pages/content.xml.lua b/res/layouts/pages/content.xml.lua index 70b5a486c..6d684b130 100644 --- a/res/layouts/pages/content.xml.lua +++ b/res/layouts/pages/content.xml.lua @@ -182,15 +182,15 @@ end local Version = {} function Version.matches_pattern(version) - for _, letter in string.gmatch(version, "%.+") do - if type(letter) ~= "number" or letter ~= "." then - return false; - end + local t = string.split(version, ".") + if #t ~= 2 and #t ~= 3 then return false end - local t = string.split(version, ".") - - return #t == 2 or #t == 3; + for i = 1, #t do + local matched = string.match(t[i], "^%d+$") + if not matched then return false end end + + return true end function Version.__equal(ver1, ver2) @@ -208,7 +208,7 @@ function Version.__less(ver1, ver2) end function Version.__greater_or_equal(ver1, ver2) - return not Version.__less(ver1, ver2) + return not Version.__less(ver2, ver1) end function Version.__less_or_equal(ver1, ver2) @@ -230,9 +230,9 @@ function Version.compare(op, ver1, ver2) local comparison_func = Version.operators[op]; if comparison_func then - return comparison_func(ver1, ver2); + return comparison_func(ver1, ver2) else - return false; + return false end end @@ -247,13 +247,12 @@ function Version.parse(version) return op, string.sub(version, #op + 1) end - return "=", version; + return "=", version end -local function compare_version(dependent_version, actual_version) +local function compare_version(op, dependent_version, actual_version) if Version.matches_pattern(dependent_version) and Version.matches_pattern(actual_version) then - local op, dep_ver = Version.parse_version(dependent_version) - Version.compare(op, dep_ver, actual_version) + return Version.compare(op, dep_ver, actual_version) elseif dependent_version == "*" or dependent_version == actual_version then return true else @@ -280,7 +279,7 @@ function check_dependencies(packinfo) local dep_pack = pack.get_info(depid) local op, ver = Version.parse(depver) - if not compare_version(depver, dep_pack.version) then + if not compare_version(op, ver, dep_pack.version) then return string.format( "%s: %s != %s (%s)", gui.str("error.dependency-version-not-met"), From 4ae3dcf1ae643e8ee7ce774976e6a78572abb7ed Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 2 Aug 2026 20:07:10 +0300 Subject: [PATCH 125/217] remove it again --- src/graphics/render/ChunksRenderer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graphics/render/ChunksRenderer.cpp b/src/graphics/render/ChunksRenderer.cpp index 7e81e0bd5..bf90f8a93 100644 --- a/src/graphics/render/ChunksRenderer.cpp +++ b/src/graphics/render/ChunksRenderer.cpp @@ -127,7 +127,7 @@ const ChunkMesh* ChunksRenderer::render( const std::shared_ptr& chunk, bool important, bool lowPriority ) { glm::ivec2 key(chunk->x, chunk->z); - if (important || true) { + if (important) { ChunkMesh mesh {}; auto voxelsBuffer = prepareVoxelsVolume(*chunk); mesh = renderer->render(chunk.get(), *voxelsBuffer); From 74cefdafbd1168f0ca678941322bdf62aa55e881 Mon Sep 17 00:00:00 2001 From: EsPaKira Date: Sun, 2 Aug 2026 21:39:33 +0400 Subject: [PATCH 126/217] fix calc_loot export --- res/content/base/modules/util.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/content/base/modules/util.lua b/res/content/base/modules/util.lua index 1044eb394..2f53ba6be 100644 --- a/res/content/base/modules/util.lua +++ b/res/content/base/modules/util.lua @@ -12,7 +12,7 @@ function util.drop(ppos, itemid, count, data, pickup_delay) }}) end -function calc_loot(loot_table) +function util.calc_loot(loot_table) local results = {} for _, loot in ipairs(loot_table) do local chance = loot.chance or 1 From a422ea1534f9bb47f0d48d16d6e7a450e9f00e16 Mon Sep 17 00:00:00 2001 From: EsPaKira Date: Sun, 2 Aug 2026 21:43:10 +0400 Subject: [PATCH 127/217] fix util --- res/content/base/modules/util.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/content/base/modules/util.lua b/res/content/base/modules/util.lua index 2f53ba6be..874e6d3d6 100644 --- a/res/content/base/modules/util.lua +++ b/res/content/base/modules/util.lua @@ -37,7 +37,7 @@ end function util.block_loot(blockid) local lootscheme = block.properties[blockid]["base:loot"] if lootscheme then - return calc_loot(lootscheme) + return util.calc_loot(lootscheme) end return {{item=block.get_picking_item(blockid), count=1}} end From 2a2169cff0cd25cf38c4bde95735c2392373dafc Mon Sep 17 00:00:00 2001 From: MihailRis Date: Sun, 2 Aug 2026 22:35:03 +0300 Subject: [PATCH 128/217] refactor: meshes build queue instead of retrieveChunk --- src/graphics/render/ChunksRenderer.cpp | 144 ++++++++++++++----------- src/graphics/render/ChunksRenderer.hpp | 53 +++++---- src/maths/util.hpp | 8 +- 3 files changed, 112 insertions(+), 93 deletions(-) diff --git a/src/graphics/render/ChunksRenderer.cpp b/src/graphics/render/ChunksRenderer.cpp index bf90f8a93..09544a358 100644 --- a/src/graphics/render/ChunksRenderer.cpp +++ b/src/graphics/render/ChunksRenderer.cpp @@ -11,8 +11,10 @@ #include "world/Level.hpp" #include "window/Camera.hpp" #include "maths/FrustumCulling.hpp" +#include "maths/util.hpp" #include "util/listutil.hpp" #include "util/ObjectsPool.hpp" +#include "util/timeutil.hpp" #include "settings.hpp" #include "content/Content.hpp" @@ -162,76 +164,63 @@ void ChunksRenderer::clear() { threadPool.clearQueue(); } -const ChunkMesh* ChunksRenderer::getOrRender( - const std::shared_ptr& chunk, bool important, bool lowPriority -) { - auto found = meshes.find(glm::ivec2(chunk->x, chunk->z)); - if (found == meshes.end()) { - return render(chunk, important, lowPriority); - } - if (chunk->flags.modified && chunk->flags.lighted) { - render(chunk, important, lowPriority); - } - return &found->second; -} - void ChunksRenderer::update() { threadPool.pullResults(); enqueuedInFrame = 0; -} -const Mesh* ChunksRenderer::retrieveChunk( - size_t index, const Camera& camera, bool culling -) { - auto chunk = chunks.getChunks()[index]; - if (chunk == nullptr) { - return nullptr; - } - if (!chunk->flags.lighted) { - const auto& found = meshes.find({chunk->x, chunk->z}); - if (found == meshes.end()) { - return nullptr; - } else { - return found->second.mesh.get(); + int width = chunks.getWidth(); + int halfWidth = width / 2; + int halfHeight = chunks.getHeight() / 2; + int centerX = chunks.getOffsetX() + halfWidth; + int centerY = chunks.getOffsetY() + halfHeight; + + meshBuildQueue.clear(); + auto& chunksArray = chunks.getChunks(); + for (int index = 0; index < chunks.getVolume(); index++) { + const auto& chunk = chunks.getChunks()[index]; + if (chunk == nullptr || !chunk->flags.lighted) { + continue; + } + int x = chunk->x; + int z = chunk->z; + if (chunk->flags.modified || meshes.find({x, z}) == meshes.end()) { + meshBuildQueue.emplace_back(x - centerX, z - centerY); } } - float distance = glm::distance( - camera.position, - glm::vec3( - (chunk->x + 0.5f) * CHUNK_W, - camera.position.y, - (chunk->z + 0.5f) * CHUNK_D - ) + std::sort( + meshBuildQueue.begin(), + meshBuildQueue.end(), + [](const glm::ivec2& a, const glm::ivec2& b) { + return util::length2(a) < util::length2(b); + } ); - auto mesh = getOrRender( - chunk, - distance < CHUNK_W * 1.5f, - distance > CHUNK_W * settings.chunks.loadDistance.get() * 0.5 - ); - if (mesh == nullptr) { - return nullptr; - } - if (chunk->flags.dirtyHeights) { - chunk->updateHeights(); - } - if (culling) { - const auto& meshAABB = mesh->meshAABB; - auto aabbMin = meshAABB.min(); - auto aabbMax = meshAABB.max(); - glm::vec3 min( - chunk->x * CHUNK_W + std::min(0.0f, aabbMin.x), - chunk->bottom, - chunk->z * CHUNK_D + std::min(0.0f, aabbMin.z) - ); - glm::vec3 max( - chunk->x * CHUNK_W + aabbMax.x, - chunk->top, - chunk->z * CHUNK_D + aabbMax.z - ); - if (!frustum.isBoxVisible(min, max)) return nullptr; + const int topN = 10; + int top = std::min(meshBuildQueue.size(), topN); + for (int i = 0; i < top; i++) { + glm::ivec2 offset = meshBuildQueue[i]; + size_t index = + (offset.y + halfHeight) * width + offset.x + halfWidth; + + auto& chunk = chunksArray[index]; + assert(chunk != nullptr); + + float distance = glm::distance( + glm::vec3 (centerX, 0.0f, centerY), + glm::vec3( + (chunk->x + 0.5f) * CHUNK_W, + 0, + (chunk->z + 0.5f) * CHUNK_D + ) + ); + bool important = distance < CHUNK_W * 1.5f; + bool lowPriority = distance > CHUNK_W * settings.chunks.loadDistance.get() * 0.5; + + if (chunk->flags.dirtyHeights) { + chunk->updateHeights(); + } + render(chunk, important, lowPriority); } - return mesh->mesh.get(); } void ChunksRenderer::drawShadowsPass( @@ -318,16 +307,43 @@ void ChunksRenderer::drawChunks( // TODO: minimize draw calls number for (int i = indices.size()-1; i >= 0; i--) { auto& chunk = chunks.getChunks()[indices[i].index]; - auto mesh = retrieveChunk(indices[i].index, camera, culling); - if (mesh == nullptr) { + if (chunk == nullptr) { continue; } + auto found = meshes.find(glm::ivec2(chunk->x, chunk->z)); + if (found == meshes.end()) { + continue; + } + auto& mesh = found->second; + if (mesh.mesh == nullptr) { + continue; + } + if (culling) { + const auto& meshAABB = mesh.meshAABB; + auto aabbMin = meshAABB.min(); + auto aabbMax = meshAABB.max(); + glm::vec3 min( + chunk->x * CHUNK_W + std::min(0.0f, aabbMin.x), + chunk->bottom, + chunk->z * CHUNK_D + std::min(0.0f, aabbMin.z) + ); + glm::vec3 max( + chunk->x * CHUNK_W + aabbMax.x, + chunk->top, + chunk->z * CHUNK_D + aabbMax.z + ); + + if (!frustum.isBoxVisible(min, max)) { + continue; + } + } + glm::vec3 coord( chunk->x * CHUNK_W + 0.5f, 0.5f, chunk->z * CHUNK_D + 0.5f ); glm::mat4 model = glm::translate(glm::mat4(1.0f), coord); shader.uniformMatrix("u_model", model); - mesh->draw(GL_TRIANGLES, glm::distance2(camera.position * glm::vec3(1, 0, 1), + mesh.mesh->draw(GL_TRIANGLES, glm::distance2(camera.position * glm::vec3(1, 0, 1), (coord + glm::vec3(CHUNK_W * 0.5f, 0.0f, CHUNK_D * 0.5f))) < denseDistance2); visibleChunks++; } diff --git a/src/graphics/render/ChunksRenderer.hpp b/src/graphics/render/ChunksRenderer.hpp index 5e6783d97..2f25875c5 100644 --- a/src/graphics/render/ChunksRenderer.hpp +++ b/src/graphics/render/ChunksRenderer.hpp @@ -1,16 +1,17 @@ #pragma once +#define GLM_ENABLE_EXPERIMENTAL + +#include "util/ThreadPool.hpp" +#include "commons.hpp" + #include #include #include #include -#define GLM_ENABLE_EXPERIMENTAL #include -#include "util/ThreadPool.hpp" -#include "commons.hpp" - template class Mesh; class Chunk; class Level; @@ -44,22 +45,6 @@ struct RendererJob { }; class ChunksRenderer { - const Chunks& chunks; - const Assets& assets; - const Frustum& frustum; - const EngineSettings& settings; - - std::unique_ptr renderer; - std::unordered_map meshes; - std::unordered_map inwork; - std::vector indices; - util::ThreadPool threadPool; - const Mesh* retrieveChunk( - size_t index, const Camera& camera, bool culling - ); - std::shared_ptr prepareVoxelsVolume(const Chunk& chunk); - - size_t enqueuedInFrame = 0; public: ChunksRenderer( const Level& level, @@ -71,16 +56,9 @@ public: ); virtual ~ChunksRenderer(); - const ChunkMesh* render( - const std::shared_ptr& chunk, bool important, bool lowPriority - ); void unload(const Chunk* chunk); void clear(); - const ChunkMesh* getOrRender( - const std::shared_ptr& chunk, bool important, bool lowPriority - ); - void drawShadowsPass( const Camera& camera, Shader& shader, const Camera& playerCamera ); @@ -92,4 +70,25 @@ public: void update(); static size_t visibleChunks; + +private: + const Chunks& chunks; + const Assets& assets; + const Frustum& frustum; + const EngineSettings& settings; + + std::unique_ptr renderer; + std::unordered_map meshes; + std::unordered_map inwork; + std::vector indices; + util::ThreadPool threadPool; + std::vector meshBuildQueue; + + size_t enqueuedInFrame = 0; + + std::shared_ptr prepareVoxelsVolume(const Chunk& chunk); + + const ChunkMesh* render( + const std::shared_ptr& chunk, bool important, bool lowPriority + ); }; diff --git a/src/maths/util.hpp b/src/maths/util.hpp index cb58212eb..c5a576f0d 100644 --- a/src/maths/util.hpp +++ b/src/maths/util.hpp @@ -109,12 +109,16 @@ namespace util { /// @return integer square of vector length /// @note glm::length2 does not support integer vectors - inline int length2(const glm::ivec3& a) { + inline long long int length2(const glm::ivec2& a) { + return a.x * a.x + a.y * a.y; + } + + inline long long int length2(const glm::ivec3& a) { return a.x * a.x + a.y * a.y + a.z * a.z; } /// @return integer square of vector length - inline int length2(int x, int y, int z) { + inline long long int length2(int x, int y, int z) { return x * x + y * y + z * z; } From 7525e31942c5d51a90246b2e04dfa3c87c5c13be Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 3 Aug 2026 00:06:19 +0300 Subject: [PATCH 129/217] cleanup ChunksRenderer --- src/graphics/render/ChunksRenderer.cpp | 34 +++++++++++++++----------- src/graphics/render/ChunksRenderer.hpp | 8 +++--- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/graphics/render/ChunksRenderer.cpp b/src/graphics/render/ChunksRenderer.cpp index 09544a358..82a7fa6d6 100644 --- a/src/graphics/render/ChunksRenderer.cpp +++ b/src/graphics/render/ChunksRenderer.cpp @@ -125,30 +125,30 @@ std::shared_ptr ChunksRenderer::prepareVoxelsVolume( return voxelsBuffer; } -const ChunkMesh* ChunksRenderer::render( - const std::shared_ptr& chunk, bool important, bool lowPriority +void ChunksRenderer::renderBlocking(const std::shared_ptr& chunk) { + glm::ivec2 key(chunk->x, chunk->z); + ChunkMesh mesh {}; + auto voxelsBuffer = prepareVoxelsVolume(*chunk); + mesh = renderer->render(chunk.get(), *voxelsBuffer); + meshes[key] = std::move(mesh); + chunk->flags.modified = false; +} + +void ChunksRenderer::render( + const std::shared_ptr& chunk, bool lowPriority ) { glm::ivec2 key(chunk->x, chunk->z); - if (important) { - ChunkMesh mesh {}; - auto voxelsBuffer = prepareVoxelsVolume(*chunk); - mesh = renderer->render(chunk.get(), *voxelsBuffer); - meshes[key] = std::move(mesh); - chunk->flags.modified = false; - return &meshes[key]; - } if (inwork.find(key) != inwork.end() || ((inwork.size() >= threadPool.getWorkersCount() || enqueuedInFrame >= MAX_CHUNKS_ENQUEUED_IN_FRAME) && lowPriority)) { - return nullptr; + return; } chunk->flags.modified = false; enqueuedInFrame++; auto voxelsBuffer = prepareVoxelsVolume(*chunk); threadPool.enqueueJob({chunk, std::move(voxelsBuffer)}); inwork[key] = true; - return nullptr; } void ChunksRenderer::unload(const Chunk* chunk) { @@ -195,6 +195,8 @@ void ChunksRenderer::update() { } ); + int loadDistance = settings.chunks.loadDistance.get(); + const int topN = 10; int top = std::min(meshBuildQueue.size(), topN); for (int i = 0; i < top; i++) { @@ -214,12 +216,16 @@ void ChunksRenderer::update() { ) ); bool important = distance < CHUNK_W * 1.5f; - bool lowPriority = distance > CHUNK_W * settings.chunks.loadDistance.get() * 0.5; + bool lowPriority = distance > CHUNK_W * loadDistance * 0.5; if (chunk->flags.dirtyHeights) { chunk->updateHeights(); } - render(chunk, important, lowPriority); + if (important) { + renderBlocking(chunk); + } else { + render(chunk, lowPriority); + } } } diff --git a/src/graphics/render/ChunksRenderer.hpp b/src/graphics/render/ChunksRenderer.hpp index 2f25875c5..5e26adc43 100644 --- a/src/graphics/render/ChunksRenderer.hpp +++ b/src/graphics/render/ChunksRenderer.hpp @@ -54,7 +54,7 @@ public: const ContentGfxCache& cache, const EngineSettings& settings ); - virtual ~ChunksRenderer(); + ~ChunksRenderer(); void unload(const Chunk* chunk); void clear(); @@ -88,7 +88,7 @@ private: std::shared_ptr prepareVoxelsVolume(const Chunk& chunk); - const ChunkMesh* render( - const std::shared_ptr& chunk, bool important, bool lowPriority - ); + void render(const std::shared_ptr& chunk, bool lowPriority); + + void renderBlocking(const std::shared_ptr& chunk); }; From e2a9d2871a588f94a8168401f6b6bda176fe684f Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 3 Aug 2026 00:06:31 +0300 Subject: [PATCH 130/217] cleanup --- src/util/ThreadPool.hpp | 1 - src/voxels/Chunk.hpp | 20 ++++++++++---------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/util/ThreadPool.hpp b/src/util/ThreadPool.hpp index dd692d203..2c743e37d 100644 --- a/src/util/ThreadPool.hpp +++ b/src/util/ThreadPool.hpp @@ -5,7 +5,6 @@ #include #include #include -#include #include #include #include diff --git a/src/voxels/Chunk.hpp b/src/voxels/Chunk.hpp index 941034073..4b2fa193d 100644 --- a/src/voxels/Chunk.hpp +++ b/src/voxels/Chunk.hpp @@ -29,16 +29,16 @@ public: voxel voxels[CHUNK_VOL] {}; std::shared_ptr lightmap; struct { - bool modified : 1; - bool ready : 1; - bool loaded : 1; - bool lighted : 1; - bool unsaved : 1; - bool loadedLights : 1; - bool entities : 1; - bool blocksData : 1; - bool dirtyHeights : 1; - bool inventoriesRemoved : 1; + bool modified : 1; // is chunk mesh should be updated + bool ready : 1; // is chunk ready for modifications (loaded / generated) + bool loaded : 1; // was chunk loaded from region + bool lighted : 1; // is lights built (chunk ready to be visualized) + bool unsaved : 1; // does chunk contain unsaved changes + bool loadedLights : 1; // was lights loaded from cache + bool entities : 1; // does chunk contain entities list changes + bool blocksData : 1; // does chunk contain block fields changes + bool dirtyHeights : 1; // is chunk bottom, top should be recalculated + bool inventoriesRemoved : 1; // was block inventories removed since the last save } flags {}; uint64_t lastRandomTickId = -1; From f8a44c023f563f2adc511356a1e7865a20b9ab7b Mon Sep 17 00:00:00 2001 From: RomanDonw Date: Mon, 3 Aug 2026 18:25:44 +1000 Subject: [PATCH 131/217] patched code after code review from MihailRis --- res/modules/internal/bytearray.lua | 37 +++++++++---------- .../lua/usertypes/lua_type_canvas.cpp | 7 +++- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/res/modules/internal/bytearray.lua b/res/modules/internal/bytearray.lua index 04212d3e1..1615d1a08 100644 --- a/res/modules/internal/bytearray.lua +++ b/res/modules/internal/bytearray.lua @@ -4,13 +4,14 @@ local FFI = ffi FFI.cdef[[ void* malloc(size_t); - void *realloc(void *, size_t); + void* realloc(void*, size_t); void free(void*); typedef struct { unsigned char* bytes; int size; int capacity; } bytearray_t; + void* memmove(void*, const void*, size_t); ]] local free = FFI.C.free @@ -172,29 +173,27 @@ local function slice(self, offset, length) end local function copy(self, srcindex, dst, dstindex, size) - if srcindex > 0 and dstindex > 0 and size > 0 and - srcindex + size - 1 <= self.size and - dstindex + size - 1 <= dst.size - then - FFI.copy(dst.bytes + dstindex - 1, self.bytes + srcindex - 1, size) + if size <= 0 then error("size of byte range must be positive non-zero integer") end + if srcindex < 1 or srcindex + size - 1 > self.size then + error("specified source byte range is out of source array bounds") end + if dstindex < 1 or dstindex + size - 1 > dst.size then + error("specified destination byte range is out of destination array bounds") + end + + FFI.copy(dst.bytes + dstindex - 1, self.bytes + srcindex - 1, size) end local function move(self, fromindex, toindex, size) - if fromindex > 0 and toindex > 0 and size > 0 and - fromindex + size - 1 <= self.size and - toindex + size - 1 <= self.size - then - if toindex >= fromindex and toindex < fromindex + size then - local buff = malloc(size) - FFI.copy(buff, self.bytes + fromindex - 1, size) - FFI.copy(self.bytes + toindex - 1, buff, size) - free(buff) - else - -- убери(-те) это ветвление, если FFI будет производить копирование не побайтово, и оставь(-те) только верхний блок без условия вообще. - FFI.copy(self.bytes + toindex - 1, self.bytes + fromindex - 1, size) - end + if size <= 0 then error("size of byte range must be positive non-zero integer") end + if fromindex < 1 or fromindex + size - 1 > self.size then + error("specified source byte range is out of array bounds") end + if toindex < 1 or toindex + size - 1 > self.size then + error("specified destination byte range is out of array bounds") + end + + FFI.C.memmove(self.bytes + self.toindex - 1, self.bytes + self.fromindex - 1, size) end local bytearray_methods = { diff --git a/src/logic/scripting/lua/usertypes/lua_type_canvas.cpp b/src/logic/scripting/lua/usertypes/lua_type_canvas.cpp index d03b4e855..928515e74 100644 --- a/src/logic/scripting/lua/usertypes/lua_type_canvas.cpp +++ b/src/logic/scripting/lua/usertypes/lua_type_canvas.cpp @@ -164,8 +164,11 @@ static int l_clear(State* L) { size_t pixelscount = image.getWidth() * image.getHeight(); uint32_t* data = reinterpret_cast(image.getData()); - if (gettop(L) == 1) std::fill(data, data + pixelscount, 0); - else std::fill(data, data + pixelscount, get_rgba(L, 2).rgba); + if (gettop(L) == 1) { + std::fill(data, data + pixelscount, 0); + } else { + std::fill(data, data + pixelscount, get_rgba(L, 2).rgba); + } return 0; } From 3eac247a6918398e6bbf92278d615eff8836c8d6 Mon Sep 17 00:00:00 2001 From: RomanDonw Date: Mon, 3 Aug 2026 22:09:11 +1000 Subject: [PATCH 132/217] eliminated hardcoded types sizes in bytearray-viewers declaratons and replaced on usage ffi.sizeof function --- res/modules/internal/bytearray.lua | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/res/modules/internal/bytearray.lua b/res/modules/internal/bytearray.lua index 1615d1a08..d58839f15 100644 --- a/res/modules/internal/bytearray.lua +++ b/res/modules/internal/bytearray.lua @@ -298,7 +298,8 @@ local function FFIBytearray_as_string(bytes) end end -local function create_FFIview_class(name, typename, typesize) +local function create_FFIview_class(name, typename) + local typesize = FFI.sizeof(typename) local ptrtype = typename .. "*" local FFIview_mt = { __index = function(self, key) @@ -343,11 +344,11 @@ local function create_FFIview_class(name, typename, typesize) end end -local FFII16view = create_FFIview_class("FFII16view", "int16_t", 2) -local FFIU16view = create_FFIview_class("FFIU16view", "uint16_t", 2) -local FFII32view = create_FFIview_class("FFII32view", "int32_t", 4) -local FFIU32view = create_FFIview_class("FFIU32view", "uint32_t", 4) -local FFIF32view = create_FFIview_class("FFIF32view", "float", 4) +local FFII16view = create_FFIview_class("FFII16view", "int16_t") +local FFIU16view = create_FFIview_class("FFIU16view", "uint16_t") +local FFII32view = create_FFIview_class("FFII32view", "int32_t") +local FFIU32view = create_FFIview_class("FFIU32view", "uint32_t") +local FFIF32view = create_FFIview_class("FFIF32view", "float") return { FFIBytearray = setmetatable(FFIBytearray, FFIBytearray), From 2c72d33485fd38b5b69b8d9f80bb1bd53f1b720f Mon Sep 17 00:00:00 2001 From: RomanDonw Date: Mon, 3 Aug 2026 22:45:48 +1000 Subject: [PATCH 133/217] improved Bytearray:fill method --- res/modules/internal/bytearray.lua | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/res/modules/internal/bytearray.lua b/res/modules/internal/bytearray.lua index d58839f15..586fa8e25 100644 --- a/res/modules/internal/bytearray.lua +++ b/res/modules/internal/bytearray.lua @@ -142,8 +142,13 @@ local function clear(self) self.size = 0 end -local function fill(self, byte) - if self.size > 0 then +local function fill(self, index, size, byte) + if index ~= nil and size ~= nil and size > 0 then + if index < 1 or index + size - 1 > self.size then + error("selected fill range is out of array bounds") + end + FFI.fill(self.bytes + index - 1, size, byte) + else FFI.fill(self.bytes, self.size, byte) end end From 5a0f63061ffc866052fa7f0d0bd190987ab447ed Mon Sep 17 00:00:00 2001 From: RomanDonw Date: Mon, 3 Aug 2026 23:41:57 +1000 Subject: [PATCH 134/217] added & upgraded Russian docs for new bytearray functions, new bytearray views (viewers) and other features --- doc/ru/scripting/bytearray.md | 37 ++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/doc/ru/scripting/bytearray.md b/doc/ru/scripting/bytearray.md index c90b6d67e..138eed347 100644 --- a/doc/ru/scripting/bytearray.md +++ b/doc/ru/scripting/bytearray.md @@ -22,11 +22,31 @@ bytes:insert(index: int, value: int) -- Удаляет элемент(ы), если передан `count`, удалит `count` кол-во элементов с выбранного индекса. bytes:remove(index: int, [опционально] count: int) --- Очищает массив +-- Очищает массив. bytes:clear() +--[[ +Заполняет массив определённым байтом полностью либо в определённом диапазоне. +Логика работы следующая: если параметр size или index равны nil, то указанным байтом будет заполнен весь массив, + иначе будет заполнен диапазон внутри массива начиная с индекса index и размером size. +В случае выхода заполняемого блока за границы массива генерирует соответствующую ошибку. +]] +bytes:fill(index: posint/nil, size: posint/nil, byte: uint8) + -- Создаёт новый Bytearray, содержащий копию части данных с offset до offset+length bytes:slice(offset: int, length: int) -> Bytearray + +--[[ +Копирует блок байт из исходного массива, начиная с индекса srcindex, в массив dst, начиная в нём с индекса dstindex. +В случае выхода за границы исходного, конечного массива, либо при некорректном значении параметра size генерирует соответствующую ошибку. +]] +bytes:copy(srcindex: posint, dst: Bytearray, dstindex: posint, size: posint) + +--[[ +Безопасно копирует перекрывающиеся области памяти (байт) в пределах одного массива. +В случае выхода за пределы массива начального или конечного диапазона байт генерирует соответствующую ошибку. +]] +bytes:move(fromindex: posint, toindex: posint, size: posint) ``` ## View @@ -35,12 +55,15 @@ bytes:slice(offset: int, length: int) -> Bytearray Не имеет своих методов, является просто объектной ссылкой на оригинальный Bytearray. -| Класс | Тип элементов | Размер | -| ------------ | ------------------ | ------- | -| `I16view` | `int16_t` | 2 байта | -| `U16view` | `uint16_t` | 2 байта | -| `I32view` | `int32_t` | 4 байта | -| `U32view` | `uint32_t` | 4 байта | +| Класс | Тип элементов | Размер* | +| ------------ | ------------------ | -------- | +| `I16view` | `int16_t` | 2 байта | +| `U16view` | `uint16_t` | 2 байта | +| `I32view` | `int32_t` | 4 байта | +| `U32view` | `uint32_t` | 4 байта | +| `F32view` | `float` | 4 байта | + +* Размеры C-шных типов являются платформозависимыми. В данной таблице предоставлены наиболее распространённые размеры типов. --- From 7c9fff19e457e4d53d7bc68cb8d871ade80fb716 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 3 Aug 2026 19:54:54 +0300 Subject: [PATCH 135/217] add '--log' command line parameter --- src/engine/CoreParameters.hpp | 1 + src/main.cpp | 10 +++++++++- src/util/command_line.cpp | 4 ++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/engine/CoreParameters.hpp b/src/engine/CoreParameters.hpp index b5d8074ce..9668ce1cd 100644 --- a/src/engine/CoreParameters.hpp +++ b/src/engine/CoreParameters.hpp @@ -12,6 +12,7 @@ struct CoreParameters { std::filesystem::path userFolder = "."; std::filesystem::path scriptFile; std::filesystem::path projectFolder; + std::filesystem::path logFile; std::string debugServerString; int tps = 20; int subProcessDepth = 0; diff --git a/src/main.cpp b/src/main.cpp index 402b829b3..4e739acde 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -7,6 +7,7 @@ #include #include +using namespace std::literals; static debug::Logger logger("main"); static void sigterm_handler(int signum) { @@ -36,7 +37,14 @@ int main(int argc, char** argv) { #ifdef NDEBUG std::signal(SIGINT, sigterm_handler); #endif - debug::Logger::init(coreParameters.userFolder.string() + "/latest.log"); + std::filesystem::path logFile = coreParameters.logFile; + if (logFile.empty()) { + logFile = coreParameters.userFolder.string() + "/latest"s + + (coreParameters.subProcessDepth > 0 + ? ".sub" + std::to_string(coreParameters.subProcessDepth) + : "") + ".log"s; + } + debug::Logger::init(logFile); platform::configure_encoding(); auto& engine = Engine::getInstance(); diff --git a/src/util/command_line.cpp b/src/util/command_line.cpp index f05231821..d5435e955 100644 --- a/src/util/command_line.cpp +++ b/src/util/command_line.cpp @@ -82,6 +82,10 @@ static bool perform_keyword( params.subProcessDepth = reader.nextInt(); return true; }, "", "sub-process depth"), + ArgC("--log", [](auto& params, auto& reader) -> bool { + params.logFile = reader.next(); + return true; + }, "", "target log file"), ArgC("--help", [](auto&, auto&) -> bool { std::cout << "VoxelCore v" << ENGINE_VERSION_STRING << "\n\n"; std::cout << "Command-line arguments:\n"; From 48a09b10be84880b3d6e9d53687d40f521d7deca Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 3 Aug 2026 20:02:30 +0300 Subject: [PATCH 136/217] update platform::new_engine_instance --- src/logic/scripting/lua/libs/libapp.cpp | 9 ++++----- src/util/platform.cpp | 9 +++++++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/logic/scripting/lua/libs/libapp.cpp b/src/logic/scripting/lua/libs/libapp.cpp index 9157b33a6..4885915ad 100644 --- a/src/logic/scripting/lua/libs/libapp.cpp +++ b/src/logic/scripting/lua/libs/libapp.cpp @@ -403,6 +403,7 @@ static int l_start_background_instance(lua::State* L) { "--script", io::resolve(scriptPath).u8string(), "--sub-depth", std::to_string(engine->getCoreParameters() .subProcessDepth + 1), + "--log", io::resolve(outputPath), }; args.emplace_back("--project"); args.emplace_back(io::resolve(engine->getProject().path).u8string()); @@ -417,11 +418,9 @@ static int l_start_background_instance(lua::State* L) { if (handle == -1) { throw std::runtime_error("sub-processes limit exceeded"); } - ::processes[handle] = platform::new_engine_instance( - std::move(args), - outputPath.empty() ? "" : io::resolve(outputPath), - true - ); + ::processes[handle] = + platform::new_engine_instance(std::move(args), "", true); + return lua::pushinteger(L, handle); } diff --git a/src/util/platform.cpp b/src/util/platform.cpp index 7b08639e9..c7b945510 100644 --- a/src/util/platform.cpp +++ b/src/util/platform.cpp @@ -28,6 +28,8 @@ #include #endif +using namespace std::literals; + namespace platform::internal { std::filesystem::path get_executable_path(); } @@ -356,7 +358,7 @@ std::unique_ptr platform::new_engine_instance( if (!outputFile.empty()) { si.dwFlags |= STARTF_USESTDHANDLES; si.hStdOutput = CreateFileW( - toWString(outputFile.u8string()).data(), + toWString(outputFile.empty() ? outputFile.u8string() : "NUL"s,).data(), GENERIC_WRITE, FILE_SHARE_WRITE | FILE_SHARE_READ, nullptr, @@ -434,9 +436,12 @@ std::unique_ptr platform::new_engine_instance( if (subProcess) { pid_t pid = fork(); + if (outputFile.empty()) { + outputFile = "/dev/null"; + } int fd = open( outputFile.string().c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644 - ); + ); if (pid == 0) { prctl(PR_SET_PDEATHSIG, SIGTERM); From 099830d2fafeb2a4846746df74deb74957dee353 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 3 Aug 2026 20:19:03 +0300 Subject: [PATCH 137/217] fix windows build --- src/logic/scripting/lua/libs/libapp.cpp | 2 +- src/util/platform.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/logic/scripting/lua/libs/libapp.cpp b/src/logic/scripting/lua/libs/libapp.cpp index 4885915ad..84f319a9c 100644 --- a/src/logic/scripting/lua/libs/libapp.cpp +++ b/src/logic/scripting/lua/libs/libapp.cpp @@ -403,7 +403,7 @@ static int l_start_background_instance(lua::State* L) { "--script", io::resolve(scriptPath).u8string(), "--sub-depth", std::to_string(engine->getCoreParameters() .subProcessDepth + 1), - "--log", io::resolve(outputPath), + "--log", io::resolve(outputPath).u8string(), }; args.emplace_back("--project"); args.emplace_back(io::resolve(engine->getProject().path).u8string()); diff --git a/src/util/platform.cpp b/src/util/platform.cpp index c7b945510..5d5e489bc 100644 --- a/src/util/platform.cpp +++ b/src/util/platform.cpp @@ -358,7 +358,7 @@ std::unique_ptr platform::new_engine_instance( if (!outputFile.empty()) { si.dwFlags |= STARTF_USESTDHANDLES; si.hStdOutput = CreateFileW( - toWString(outputFile.empty() ? outputFile.u8string() : "NUL"s,).data(), + toWString(outputFile.empty() ? outputFile.u8string() : "NUL"s).data(), GENERIC_WRITE, FILE_SHARE_WRITE | FILE_SHARE_READ, nullptr, From d6923b0e0f2185bb916fcf20b728af5b11aa6ce0 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Mon, 3 Aug 2026 20:36:16 +0300 Subject: [PATCH 138/217] fix windows build again --- src/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.cpp b/src/main.cpp index 4e739acde..9d8d8e551 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -44,7 +44,7 @@ int main(int argc, char** argv) { ? ".sub" + std::to_string(coreParameters.subProcessDepth) : "") + ".log"s; } - debug::Logger::init(logFile); + debug::Logger::init(logFile.u8string()); platform::configure_encoding(); auto& engine = Engine::getInstance(); From b30207917fb614c941bf8c7b8675fb16f9dc4fb3 Mon Sep 17 00:00:00 2001 From: RomanDonw <126128023+RomanDonw@users.noreply.github.com> Date: Mon, 3 Aug 2026 23:59:28 +1000 Subject: [PATCH 139/217] made Russian docs more exact --- doc/ru/scripting/bytearray.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/doc/ru/scripting/bytearray.md b/doc/ru/scripting/bytearray.md index 138eed347..50ec15d9d 100644 --- a/doc/ru/scripting/bytearray.md +++ b/doc/ru/scripting/bytearray.md @@ -55,15 +55,15 @@ bytes:move(fromindex: posint, toindex: posint, size: posint) Не имеет своих методов, является просто объектной ссылкой на оригинальный Bytearray. -| Класс | Тип элементов | Размер* | -| ------------ | ------------------ | -------- | -| `I16view` | `int16_t` | 2 байта | -| `U16view` | `uint16_t` | 2 байта | -| `I32view` | `int32_t` | 4 байта | -| `U32view` | `uint32_t` | 4 байта | -| `F32view` | `float` | 4 байта | +| Класс | Тип элементов | Размер | +| ------------ | ------------------ | --------- | +| `I16view` | `int16_t` | 2 байта | +| `U16view` | `uint16_t` | 2 байта | +| `I32view` | `int32_t` | 4 байта | +| `U32view` | `uint32_t` | 4 байта | +| `F32view` | `float` | 4 байта* | -* Размеры C-шных типов являются платформозависимыми. В данной таблице предоставлены наиболее распространённые размеры типов. +* Размеры "оригинальных" C-шных типов являются платформозависимыми и их точный размер не описан в стандарте языка. В данной таблице предоставлены наиболее распространённые размеры типов. --- @@ -82,4 +82,4 @@ print(i16[3]) -- -6 (signed вьюшка) for _, num in ipairs(i16) do print(num) end -- 1; 2; -6 -``` \ No newline at end of file +``` From e0afe5c6499f7b24e3bad0b03efc7060b0e862ee Mon Sep 17 00:00:00 2001 From: RomanDonw Date: Tue, 4 Aug 2026 17:18:58 +1000 Subject: [PATCH 140/217] added typesize method for bytearray views (viewers) (both to Lua and docs (only Russian for now)) --- doc/ru/scripting/bytearray.md | 7 +++++++ res/modules/internal/bytearray.lua | 3 +++ 2 files changed, 10 insertions(+) diff --git a/doc/ru/scripting/bytearray.md b/doc/ru/scripting/bytearray.md index 50ec15d9d..b16b3fd39 100644 --- a/doc/ru/scripting/bytearray.md +++ b/doc/ru/scripting/bytearray.md @@ -65,6 +65,13 @@ bytes:move(fromindex: posint, toindex: posint, size: posint) * Размеры "оригинальных" C-шных типов являются платформозависимыми и их точный размер не описан в стандарте языка. В данной таблице предоставлены наиболее распространённые размеры типов. +### Специфические методы "вьюшек" + +```lua +-- Возвращает размер типа вьюшки (полезно для вьюшек, использующих C-шные типы нефиксированного размера: те же float или double). +view:typesize() -> posint +``` + --- ### Пример использования diff --git a/res/modules/internal/bytearray.lua b/res/modules/internal/bytearray.lua index 586fa8e25..556085515 100644 --- a/res/modules/internal/bytearray.lua +++ b/res/modules/internal/bytearray.lua @@ -339,6 +339,9 @@ local function create_FFIview_class(name, typename) return i, ptr[i - 1] end end + end, + typesize = function() + return typesize end } return function (bytes) From 9fc94e40b20f6f686b4113dcd7fe530f04020fba Mon Sep 17 00:00:00 2001 From: RomanDonw Date: Tue, 4 Aug 2026 17:41:24 +1000 Subject: [PATCH 141/217] renamed bytearray F32view to FLT view & improved bytearray Russian docs --- doc/ru/scripting/bytearray.md | 9 +++++++-- res/modules/internal/bytearray.lua | 4 ++-- res/scripts/stdmin.lua | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/doc/ru/scripting/bytearray.md b/doc/ru/scripting/bytearray.md index b16b3fd39..032182891 100644 --- a/doc/ru/scripting/bytearray.md +++ b/doc/ru/scripting/bytearray.md @@ -2,6 +2,8 @@ *Bytearray* - динамический байтовый массив, реализованный через LuaJIT FFI. По нему можно итерироваться стандартными функциями **pairs** и **ipairs** +Примечание касаемо условных обозначений в документации: псведотип `posint` означает натуральное (положительное ненулевое целое) число. + ## Основное ### Создание массива ```lua @@ -61,14 +63,17 @@ bytes:move(fromindex: posint, toindex: posint, size: posint) | `U16view` | `uint16_t` | 2 байта | | `I32view` | `int32_t` | 4 байта | | `U32view` | `uint32_t` | 4 байта | -| `F32view` | `float` | 4 байта* | +| `FLTview` | `float` | 4 байта* | * Размеры "оригинальных" C-шных типов являются платформозависимыми и их точный размер не описан в стандарте языка. В данной таблице предоставлены наиболее распространённые размеры типов. ### Специфические методы "вьюшек" ```lua --- Возвращает размер типа вьюшки (полезно для вьюшек, использующих C-шные типы нефиксированного размера: те же float или double). +--[[ +Возвращает размер типа вьюшки. Результат тот же, что и от применения оператора sizeof() из языка C для данного типа. + Полезно для вьюшек, использующих C-шные типы нефиксированного размера, например те же float или double. +]] view:typesize() -> posint ``` diff --git a/res/modules/internal/bytearray.lua b/res/modules/internal/bytearray.lua index 556085515..27472f491 100644 --- a/res/modules/internal/bytearray.lua +++ b/res/modules/internal/bytearray.lua @@ -356,7 +356,7 @@ local FFII16view = create_FFIview_class("FFII16view", "int16_t") local FFIU16view = create_FFIview_class("FFIU16view", "uint16_t") local FFII32view = create_FFIview_class("FFII32view", "int32_t") local FFIU32view = create_FFIview_class("FFIU32view", "uint32_t") -local FFIF32view = create_FFIview_class("FFIF32view", "float") +local FFIFLTview = create_FFIview_class("FFIFLTview", "float") return { FFIBytearray = setmetatable(FFIBytearray, FFIBytearray), @@ -366,5 +366,5 @@ return { FFII16view = FFII16view, FFIU32view = FFIU32view, FFII32view = FFII32view, - FFIF32view = FFIF32view, + FFIFLTview = FFIFLTview, } diff --git a/res/scripts/stdmin.lua b/res/scripts/stdmin.lua index 01b61efc6..a7c7bdbd2 100644 --- a/res/scripts/stdmin.lua +++ b/res/scripts/stdmin.lua @@ -314,7 +314,7 @@ U16view = bytearray.FFIU16view I16view = bytearray.FFII16view U32view = bytearray.FFIU32view I32view = bytearray.FFII32view -F32view = bytearray.FFIF32view +FLTview = bytearray.FFIFLTview Bytearray_construct = function(...) return Bytearray(...) end bit.compile = require "core:bitwise/compiler" From 457fc573492dd9ec2f9399386bd1e1070c723830 Mon Sep 17 00:00:00 2001 From: RomanDonw Date: Tue, 4 Aug 2026 18:07:11 +1000 Subject: [PATCH 142/217] added more new views (viewers) for bytearray (again both to Lua & Russian docs) --- doc/ru/scripting/bytearray.md | 10 ++++++---- res/modules/internal/bytearray.lua | 8 ++++++++ res/scripts/stdmin.lua | 4 ++++ 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/doc/ru/scripting/bytearray.md b/doc/ru/scripting/bytearray.md index 032182891..d15cd6a58 100644 --- a/doc/ru/scripting/bytearray.md +++ b/doc/ru/scripting/bytearray.md @@ -55,15 +55,19 @@ bytes:move(fromindex: posint, toindex: posint, size: posint) Это "вьюшки" поверх Bytearray, которые интерпретируют его байты как массив чисел другого размера - **без копирования данных**. -Не имеет своих методов, является просто объектной ссылкой на оригинальный Bytearray. +Практически не имеют своих методов, являются простыми объектными ссылками на оригинальный Bytearray. | Класс | Тип элементов | Размер | | ------------ | ------------------ | --------- | +| `I8view` | `int8_t` | 1 байт | | `I16view` | `int16_t` | 2 байта | | `U16view` | `uint16_t` | 2 байта | | `I32view` | `int32_t` | 4 байта | | `U32view` | `uint32_t` | 4 байта | +| `I64view` | `int64_t` | 8 байт | +| `U64view` | `uint64_t` | 8 байт | | `FLTview` | `float` | 4 байта* | +| `DBLview` | `double` | 8 байт* | * Размеры "оригинальных" C-шных типов являются платформозависимыми и их точный размер не описан в стандарте языка. В данной таблице предоставлены наиболее распространённые размеры типов. @@ -77,9 +81,7 @@ bytes:move(fromindex: posint, toindex: posint, size: posint) view:typesize() -> posint ``` ---- - -### Пример использования +## Пример использования ```lua local bytes = Bytearray({1, 0, 2, 0, 250, 255}) diff --git a/res/modules/internal/bytearray.lua b/res/modules/internal/bytearray.lua index 27472f491..42f6bddcd 100644 --- a/res/modules/internal/bytearray.lua +++ b/res/modules/internal/bytearray.lua @@ -352,19 +352,27 @@ local function create_FFIview_class(name, typename) end end +local FFII8view = create_FFIview_class("FFII8view", "int8_t") local FFII16view = create_FFIview_class("FFII16view", "int16_t") local FFIU16view = create_FFIview_class("FFIU16view", "uint16_t") local FFII32view = create_FFIview_class("FFII32view", "int32_t") local FFIU32view = create_FFIview_class("FFIU32view", "uint32_t") +local FFII64view = create_FFIview_class("FFII32view", "int64_t") +local FFIU64view = create_FFIview_class("FFIU32view", "uint64_t") local FFIFLTview = create_FFIview_class("FFIFLTview", "float") +local FFIDBLview = create_FFIview_class("FFIDBLview", "double") return { FFIBytearray = setmetatable(FFIBytearray, FFIBytearray), FFIBytearray_as_string = FFIBytearray_as_string, FFIBytearray_as_ptr = FFIBytearray_as_ptr, + FFII8view = FFII8view, FFIU16view = FFIU16view, FFII16view = FFII16view, FFIU32view = FFIU32view, FFII32view = FFII32view, + FFIU64view = FFIU64view, + FFII64view = FFII64view, FFIFLTview = FFIFLTview, + FFIDBLview = FFIDBLview, } diff --git a/res/scripts/stdmin.lua b/res/scripts/stdmin.lua index a7c7bdbd2..93bdf118e 100644 --- a/res/scripts/stdmin.lua +++ b/res/scripts/stdmin.lua @@ -310,11 +310,15 @@ local bytearray = require "core:internal/bytearray" Bytearray = bytearray.FFIBytearray Bytearray_as_string = bytearray.FFIBytearray_as_string Bytearray_as_ptr = bytearray.FFIBytearray_as_ptr +I8view = bytearray.FFII8view U16view = bytearray.FFIU16view I16view = bytearray.FFII16view U32view = bytearray.FFIU32view I32view = bytearray.FFII32view +U64view = bytearray.FFIU64view +I64view = bytearray.FFII64view FLTview = bytearray.FFIFLTview +DBLview = bytearray.FFIDBLview Bytearray_construct = function(...) return Bytearray(...) end bit.compile = require "core:bitwise/compiler" From e203b5ef4cc77fd812e5e8f0e3775064ab4cb5ab Mon Sep 17 00:00:00 2001 From: RomanDonw Date: Tue, 4 Aug 2026 19:50:55 +1000 Subject: [PATCH 143/217] fixed two (2) typos in Bytearray:move method implementation --- res/modules/internal/bytearray.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/modules/internal/bytearray.lua b/res/modules/internal/bytearray.lua index 42f6bddcd..cfae8b53d 100644 --- a/res/modules/internal/bytearray.lua +++ b/res/modules/internal/bytearray.lua @@ -198,7 +198,7 @@ local function move(self, fromindex, toindex, size) error("specified destination byte range is out of array bounds") end - FFI.C.memmove(self.bytes + self.toindex - 1, self.bytes + self.fromindex - 1, size) + FFI.C.memmove(self.bytes + toindex - 1, self.bytes + fromindex - 1, size) end local bytearray_methods = { From 5843e5828b5e568ed271f55852eb1329e071e417 Mon Sep 17 00:00:00 2001 From: RomanDonw Date: Tue, 4 Aug 2026 20:25:15 +1000 Subject: [PATCH 144/217] upgraded bytearray's test (added testing new bytearray functions and views/viewers) --- dev/tests/bytearray.lua | 64 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/dev/tests/bytearray.lua b/dev/tests/bytearray.lua index 415768c65..2a3d6140b 100644 --- a/dev/tests/bytearray.lua +++ b/dev/tests/bytearray.lua @@ -34,3 +34,67 @@ arr:insert(2, {5, 6}) assert(arr[#arr] == 25) assert(arr[2] == 5) assert(arr[3] == 6) + +-- ============================= + +local arr2 = Bytearray({254, 255, 255, 255, 255, 255, 255, 255}) +--assert(U64view(arr2)[1] == 2 ^ 64 - 2) +assert(I64view(arr2)[1] == -2) +assert(U32view(arr2)[1] == 2 ^ 32 - 2) +assert(I32view(arr2)[1] == -2) +assert(U16view(arr2)[1] == 2 ^ 16 - 2) +assert(I16view(arr2)[1] == -2) +assert(I8view(arr2)[1] == -2) + +assert(I8view(arr2)[2] == -1) +assert(I32view(arr2)[2] == -1) + +local arr3 = Bytearray({0xFF, 0x7F}) +local arr3i16 = I16view(arr3) +local arr3u16 = U16view(arr3) +assert(arr3i16[1] == 32767) + +arr3u16[1] = arr3u16[1] + 1 +assert(arr3i16[1] == -32768) +assert(arr3u16[1] == 32768) + +local arr4 = Bytearray({0x9A, 0x99, 0x59, 0x40}) -- approx. equal 3.4 in hexadecimal representation of single-precision float number. +local arr4flt = FLTview(arr4) +local arr4u32 = U32view(arr4) +assert(arr4flt[1] == 3.4000000953674316) +assert(arr4u32[1] == 0x4059999A) + +arr4flt[1] = arr4flt[1] + 0.1 +assert(arr4flt[1] == 3.5) +assert(arr4u32[1] == 0x40600000) -- hexadecimal representation of 3.5 single-precision float number. + +-- ============================================= + +function barrtostr(barr) + local outstr = "" + for i = 1, #barr do + local lastfmt = "" + if i ~= #barr then lastfmt = "|" end + outstr = outstr .. barr[i] .. lastfmt + end + return outstr +end + +local arr5 = Bytearray({0, 5, 34, 87, 21, 0, 210}) +assert(barrtostr(arr5) == "0|5|34|87|21|0|210") +arr5:move(2, 3, 4) +assert(barrtostr(arr5) == "0|5|5|34|87|21|210") + +local arr6 = Bytearray({1, 2, 3, 4, 6, 7, 8}) +assert(barrtostr(arr6) == "1|2|3|4|6|7|8") +arr5:copy(4, arr6, 2, 3) +assert(barrtostr(arr6) == "1|34|87|21|6|7|8") + +arr5:fill(nil, nil, 192) +assert(barrtostr(arr5) == "192|192|192|192|192|192|192") +arr5:fill(nil, 3, 233) +assert(barrtostr(arr5) == "233|233|233|233|233|233|233") +arr5:fill(2, nil, 254) +assert(barrtostr(arr5) == "254|254|254|254|254|254|254") +arr5:fill(2, 2, 66) +assert(barrtostr(arr5) == "254|66|66|254|254|254|254") \ No newline at end of file From 074f01b1bb1fae161fd83d3f9061594af2c6ce71 Mon Sep 17 00:00:00 2001 From: RomanDonw Date: Tue, 4 Aug 2026 22:40:59 +1000 Subject: [PATCH 145/217] upgraded bytearray test & added DBLview testing section --- dev/tests/bytearray.lua | 85 +++++++++++++++++++++-------------------- 1 file changed, 44 insertions(+), 41 deletions(-) diff --git a/dev/tests/bytearray.lua b/dev/tests/bytearray.lua index 2a3d6140b..cbd537100 100644 --- a/dev/tests/bytearray.lua +++ b/dev/tests/bytearray.lua @@ -37,36 +37,39 @@ assert(arr[3] == 6) -- ============================= -local arr2 = Bytearray({254, 255, 255, 255, 255, 255, 255, 255}) ---assert(U64view(arr2)[1] == 2 ^ 64 - 2) -assert(I64view(arr2)[1] == -2) -assert(U32view(arr2)[1] == 2 ^ 32 - 2) -assert(I32view(arr2)[1] == -2) -assert(U16view(arr2)[1] == 2 ^ 16 - 2) -assert(I16view(arr2)[1] == -2) -assert(I8view(arr2)[1] == -2) +arr = Bytearray({254, 255, 255, 255, 255, 255, 255, 255}) +--assert(U64view(arr)[1] == 2 ^ 64 - 2) +assert(I64view(arr)[1] == -2) +assert(U32view(arr)[1] == 2 ^ 32 - 2) +assert(I32view(arr)[1] == -2) +assert(U16view(arr)[1] == 2 ^ 16 - 2) +assert(I16view(arr)[1] == -2) +assert(I8view(arr)[1] == -2) -assert(I8view(arr2)[2] == -1) -assert(I32view(arr2)[2] == -1) +assert(I8view(arr)[2] == -1) +assert(I32view(arr)[2] == -1) -local arr3 = Bytearray({0xFF, 0x7F}) -local arr3i16 = I16view(arr3) -local arr3u16 = U16view(arr3) -assert(arr3i16[1] == 32767) +arr = Bytearray({0xFF, 0x7F}) +local arri16 = I16view(arr) +local arru16 = U16view(arr) +assert(arri16[1] == 32767) -arr3u16[1] = arr3u16[1] + 1 -assert(arr3i16[1] == -32768) -assert(arr3u16[1] == 32768) +arru16[1] = arru16[1] + 1 +assert(arri16[1] == -32768) +assert(arru16[1] == 32768) -local arr4 = Bytearray({0x9A, 0x99, 0x59, 0x40}) -- approx. equal 3.4 in hexadecimal representation of single-precision float number. -local arr4flt = FLTview(arr4) -local arr4u32 = U32view(arr4) -assert(arr4flt[1] == 3.4000000953674316) -assert(arr4u32[1] == 0x4059999A) +arr = Bytearray({0x9A, 0x99, 0x59, 0x40}) -- approx. equal 3.4 in hexadecimal representation of single-precision float number. +local arrflt = FLTview(arr) +local arru32 = U32view(arr) +assert(arrflt[1] == 3.4000000953674316) +assert(arru32[1] == 0x4059999A) -arr4flt[1] = arr4flt[1] + 0.1 -assert(arr4flt[1] == 3.5) -assert(arr4u32[1] == 0x40600000) -- hexadecimal representation of 3.5 single-precision float number. +arrflt[1] = arrflt[1] + 0.1 +assert(arrflt[1] == 3.5) +assert(arru32[1] == 0x40600000) -- hexadecimal representation of 3.5 single-precision float number. + +arr = Bytearray({0x18, 0x2D, 0x44, 0x54, 0xFB, 0x21, 9, 64}) -- hexadecimal representation of 3.141592653589793 double-precision float number. +assert(DBLview(arr)[1] == 3.141592653589793) -- ============================================= @@ -80,21 +83,21 @@ function barrtostr(barr) return outstr end -local arr5 = Bytearray({0, 5, 34, 87, 21, 0, 210}) -assert(barrtostr(arr5) == "0|5|34|87|21|0|210") -arr5:move(2, 3, 4) -assert(barrtostr(arr5) == "0|5|5|34|87|21|210") +arr = Bytearray({0, 5, 34, 87, 21, 0, 210}) +assert(barrtostr(arr) == "0|5|34|87|21|0|210") +arr:move(2, 3, 4) +assert(barrtostr(arr) == "0|5|5|34|87|21|210") -local arr6 = Bytearray({1, 2, 3, 4, 6, 7, 8}) -assert(barrtostr(arr6) == "1|2|3|4|6|7|8") -arr5:copy(4, arr6, 2, 3) -assert(barrtostr(arr6) == "1|34|87|21|6|7|8") +local arr2 = Bytearray({1, 2, 3, 4, 6, 7, 8}) +assert(barrtostr(arr2) == "1|2|3|4|6|7|8") +arr:copy(4, arr2, 2, 3) +assert(barrtostr(arr2) == "1|34|87|21|6|7|8") -arr5:fill(nil, nil, 192) -assert(barrtostr(arr5) == "192|192|192|192|192|192|192") -arr5:fill(nil, 3, 233) -assert(barrtostr(arr5) == "233|233|233|233|233|233|233") -arr5:fill(2, nil, 254) -assert(barrtostr(arr5) == "254|254|254|254|254|254|254") -arr5:fill(2, 2, 66) -assert(barrtostr(arr5) == "254|66|66|254|254|254|254") \ No newline at end of file +arr:fill(nil, nil, 192) +assert(barrtostr(arr) == "192|192|192|192|192|192|192") +arr:fill(nil, 3, 233) +assert(barrtostr(arr) == "233|233|233|233|233|233|233") +arr:fill(2, nil, 254) +assert(barrtostr(arr) == "254|254|254|254|254|254|254") +arr:fill(2, 2, 66) +assert(barrtostr(arr) == "254|66|66|254|254|254|254") \ No newline at end of file From 7a88bd454a98bfc3ae8c71b4198e2f8dd16b7ca3 Mon Sep 17 00:00:00 2001 From: RomanDonw <126128023+RomanDonw@users.noreply.github.com> Date: Wed, 5 Aug 2026 00:39:21 +1000 Subject: [PATCH 146/217] fixed typo in FFIU64view & FFII64view bytearray views (viewers) declaration (bytearray.lua) --- res/modules/internal/bytearray.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/modules/internal/bytearray.lua b/res/modules/internal/bytearray.lua index cfae8b53d..0908b8c9a 100644 --- a/res/modules/internal/bytearray.lua +++ b/res/modules/internal/bytearray.lua @@ -357,8 +357,8 @@ local FFII16view = create_FFIview_class("FFII16view", "int16_t") local FFIU16view = create_FFIview_class("FFIU16view", "uint16_t") local FFII32view = create_FFIview_class("FFII32view", "int32_t") local FFIU32view = create_FFIview_class("FFIU32view", "uint32_t") -local FFII64view = create_FFIview_class("FFII32view", "int64_t") -local FFIU64view = create_FFIview_class("FFIU32view", "uint64_t") +local FFII64view = create_FFIview_class("FFII64view", "int64_t") +local FFIU64view = create_FFIview_class("FFIU64view", "uint64_t") local FFIFLTview = create_FFIview_class("FFIFLTview", "float") local FFIDBLview = create_FFIview_class("FFIDBLview", "double") From 7f04d2884e7273f11ede01ef3f943aa139fca0c3 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 4 Aug 2026 18:54:37 +0300 Subject: [PATCH 147/217] fix view classes static typesize method --- res/modules/internal/bytearray.lua | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/res/modules/internal/bytearray.lua b/res/modules/internal/bytearray.lua index 0908b8c9a..896f43188 100644 --- a/res/modules/internal/bytearray.lua +++ b/res/modules/internal/bytearray.lua @@ -340,16 +340,20 @@ local function create_FFIview_class(name, typename) end end end, - typesize = function() - return typesize - end } - return function (bytes) - local x = setmetatable({ - bytes=bytes, - }, FFIview_mt) - return x - end + return setmetatable({}, { + __call = function (self, bytes) + local x = setmetatable({ + bytes=bytes, + }, FFIview_mt) + return x + end, + __index = { + typesize = function() + return typesize + end + } + }) end local FFII8view = create_FFIview_class("FFII8view", "int8_t") From 5f94e707ea9fdd53d6272b5c718b8c226222c8ee Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 4 Aug 2026 18:55:10 +0300 Subject: [PATCH 148/217] update bytearray test --- dev/tests/bytearray.lua | 58 ++++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/dev/tests/bytearray.lua b/dev/tests/bytearray.lua index cbd537100..0d1bdb6ae 100644 --- a/dev/tests/bytearray.lua +++ b/dev/tests/bytearray.lua @@ -18,55 +18,55 @@ Bytearray.insert(arr, {5, 3, 6}) assert(#arr == 12) Bytearray.insert(arr, 2, 8) -assert(#arr == 13) +asserts.equals(13, #arr) for i=1,10 do assert(arr[i] == 10 - i) end print(#arr, arr:get_capacity()) arr:trim() -assert(#arr == arr:get_capacity()) +asserts.equals(#arr, arr:get_capacity()) arr = Bytearray({0, 2, 7, 1, 16, 75, 25}) assert(arr[6] == 75) arr:insert(2, {5, 6}) -assert(arr[#arr] == 25) -assert(arr[2] == 5) -assert(arr[3] == 6) +asserts.equals(25, arr[#arr]) +asserts.equals(5, arr[2]) +asserts.equals(6, arr[3]) -- ============================= arr = Bytearray({254, 255, 255, 255, 255, 255, 255, 255}) ---assert(U64view(arr)[1] == 2 ^ 64 - 2) -assert(I64view(arr)[1] == -2) -assert(U32view(arr)[1] == 2 ^ 32 - 2) -assert(I32view(arr)[1] == -2) -assert(U16view(arr)[1] == 2 ^ 16 - 2) -assert(I16view(arr)[1] == -2) -assert(I8view(arr)[1] == -2) +-- asserts.equals(U64view(arr)[1], 2 ^ 64 - 2) +asserts.equals(I64view(arr)[1], -2) +asserts.equals(U32view(arr)[1], 2 ^ 32 - 2) +asserts.equals(I32view(arr)[1], -2) +asserts.equals(U16view(arr)[1], 2 ^ 16 - 2) +asserts.equals(I16view(arr)[1], -2) +asserts.equals(I8view(arr)[1], -2) -assert(I8view(arr)[2] == -1) -assert(I32view(arr)[2] == -1) +asserts.equals(I8view(arr)[2], -1) +asserts.equals(I32view(arr)[2], -1) arr = Bytearray({0xFF, 0x7F}) local arri16 = I16view(arr) local arru16 = U16view(arr) -assert(arri16[1] == 32767) +asserts.equals(arri16[1], 32767) arru16[1] = arru16[1] + 1 -assert(arri16[1] == -32768) -assert(arru16[1] == 32768) +asserts.equals(arri16[1], -32768) +asserts.equals(arru16[1], 32768) arr = Bytearray({0x9A, 0x99, 0x59, 0x40}) -- approx. equal 3.4 in hexadecimal representation of single-precision float number. local arrflt = FLTview(arr) local arru32 = U32view(arr) -assert(arrflt[1] == 3.4000000953674316) -assert(arru32[1] == 0x4059999A) +asserts.equals(arrflt[1], 3.4000000953674316) +asserts.equals(arru32[1], 0x4059999A) arrflt[1] = arrflt[1] + 0.1 -assert(arrflt[1] == 3.5) -assert(arru32[1] == 0x40600000) -- hexadecimal representation of 3.5 single-precision float number. +asserts.equals(arrflt[1], 3.5) +asserts.equals(arru32[1], 0x40600000) -- hexadecimal representation of 3.5 single-precision float number. arr = Bytearray({0x18, 0x2D, 0x44, 0x54, 0xFB, 0x21, 9, 64}) -- hexadecimal representation of 3.141592653589793 double-precision float number. assert(DBLview(arr)[1] == 3.141592653589793) @@ -84,20 +84,20 @@ function barrtostr(barr) end arr = Bytearray({0, 5, 34, 87, 21, 0, 210}) -assert(barrtostr(arr) == "0|5|34|87|21|0|210") +asserts.equals(barrtostr(arr), "0|5|34|87|21|0|210") arr:move(2, 3, 4) -assert(barrtostr(arr) == "0|5|5|34|87|21|210") +asserts.equals(barrtostr(arr), "0|5|5|34|87|21|210") local arr2 = Bytearray({1, 2, 3, 4, 6, 7, 8}) -assert(barrtostr(arr2) == "1|2|3|4|6|7|8") +asserts.equals(barrtostr(arr2), "1|2|3|4|6|7|8") arr:copy(4, arr2, 2, 3) -assert(barrtostr(arr2) == "1|34|87|21|6|7|8") +asserts.equals(barrtostr(arr2), "1|34|87|21|6|7|8") arr:fill(nil, nil, 192) -assert(barrtostr(arr) == "192|192|192|192|192|192|192") +asserts.equals(barrtostr(arr), "192|192|192|192|192|192|192") arr:fill(nil, 3, 233) -assert(barrtostr(arr) == "233|233|233|233|233|233|233") +asserts.equals(barrtostr(arr), "233|233|233|233|233|233|233") arr:fill(2, nil, 254) -assert(barrtostr(arr) == "254|254|254|254|254|254|254") +asserts.equals(barrtostr(arr), "254|254|254|254|254|254|254") arr:fill(2, 2, 66) -assert(barrtostr(arr) == "254|66|66|254|254|254|254") \ No newline at end of file +asserts.equals(barrtostr(arr), "254|66|66|254|254|254|254") From 5422eb7ccc4ce7b8cf69b30c2f58a57f23b0b271 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 4 Aug 2026 19:19:50 +0300 Subject: [PATCH 149/217] add ctypes library --- res/modules/internal/ctypes.lua | 38 +++++++++++++++++++++++++++++++++ res/scripts/stdmin.lua | 2 ++ 2 files changed, 40 insertions(+) create mode 100644 res/modules/internal/ctypes.lua diff --git a/res/modules/internal/ctypes.lua b/res/modules/internal/ctypes.lua new file mode 100644 index 000000000..032ea34b5 --- /dev/null +++ b/res/modules/internal/ctypes.lua @@ -0,0 +1,38 @@ +local FFI = ffi + +FFI.cdef[[ + unsigned long long strtoull(const char* nptr, char** endptr, int base); + long long strtoll(const char* nptr, char** endptr, int base); +]] + +local this = {} + +local function create_integral_type(ctype_name, signed) + local parse_func = signed and FFI.C.strtoll or FFI.C.strtoull + return function(value, base) + value = value or 0 + local typename = type(value) + if typename == "number" then + return FFI.cast(ctype_name, value) + elseif typename == "string" then + if type(base) ~= "number" then + base = 10 + end + return parse_func(value, nil, base) + end + error(string.format("invalid value type: %s", typename)) + end +end + + +this.uint8 = create_integral_type("uint8_t", false) +this.uint16 = create_integral_type("uint16_t", false) +this.uint32 = create_integral_type("uint32_t", false) +this.uint64 = create_integral_type("uint64_t", false) + +this.int8 = create_integral_type("int8_t", true) +this.int16 = create_integral_type("int16_t", true) +this.int32 = create_integral_type("int32_t", true) +this.int64 = create_integral_type("int64_t", true) + +return this diff --git a/res/scripts/stdmin.lua b/res/scripts/stdmin.lua index 196bf8942..03de8cbaa 100644 --- a/res/scripts/stdmin.lua +++ b/res/scripts/stdmin.lua @@ -322,6 +322,8 @@ FLTview = bytearray.FFIFLTview DBLview = bytearray.FFIDBLview Bytearray_construct = function(...) return Bytearray(...) end +ctypes = require "core:internal/ctypes" + bit.compile = require "core:bitwise/compiler" bit.execute = require "core:bitwise/executor" From 40a61307b32c9b2c386346c4f17f415cd1bede93 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 4 Aug 2026 19:54:08 +0300 Subject: [PATCH 150/217] update bytearray test --- dev/tests/bytearray.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/tests/bytearray.lua b/dev/tests/bytearray.lua index 0d1bdb6ae..71e895d37 100644 --- a/dev/tests/bytearray.lua +++ b/dev/tests/bytearray.lua @@ -38,7 +38,7 @@ asserts.equals(6, arr[3]) -- ============================= arr = Bytearray({254, 255, 255, 255, 255, 255, 255, 255}) --- asserts.equals(U64view(arr)[1], 2 ^ 64 - 2) +asserts.equals(U64view(arr)[1], ctypes.uint64(2) ^ 64 - 2) asserts.equals(I64view(arr)[1], -2) asserts.equals(U32view(arr)[1], 2 ^ 32 - 2) asserts.equals(I32view(arr)[1], -2) From 4a381ec2f4c5638659eaae7422e0b6648d1b8add Mon Sep 17 00:00:00 2001 From: MihailRis Date: Tue, 4 Aug 2026 20:24:58 +0300 Subject: [PATCH 151/217] add ctypes docs (minimal) --- doc/en/scripting.md | 1 + doc/en/scripting/builtins/libctypes.md | 28 ++++++++++++++++++++++++++ doc/ru/scripting.md | 1 + doc/ru/scripting/builtins/libctypes.md | 28 ++++++++++++++++++++++++++ 4 files changed, 58 insertions(+) create mode 100644 doc/en/scripting/builtins/libctypes.md create mode 100644 doc/ru/scripting/builtins/libctypes.md diff --git a/doc/en/scripting.md b/doc/en/scripting.md index 09cf40ef4..2072d4353 100644 --- a/doc/en/scripting.md +++ b/doc/en/scripting.md @@ -16,6 +16,7 @@ Subsections: - [block](scripting/builtins/libblock.md) - [byteutil](scripting/builtins/libbyteutil.md) - [cameras](scripting/builtins/libcameras.md) + - [ctypes](scripting/builtins/libctypes.md) - [entities](scripting/builtins/libentities.md) - [file](scripting/builtins/libfile.md) - [gfx.blockwraps](scripting/builtins/libgfx-blockwraps.md) diff --git a/doc/en/scripting/builtins/libctypes.md b/doc/en/scripting/builtins/libctypes.md new file mode 100644 index 000000000..5517af0fa --- /dev/null +++ b/doc/en/scripting/builtins/libctypes.md @@ -0,0 +1,28 @@ +# *ctypes* Library + +A library for safe work with C types. + +| ctypes.\ | C-Type | Minimum | Maximum | +| --------------- | -------- | -------------------- | -------------------- | +| uint8 | uint8_t | 0 | 255 | +| uint16 | uint16_t | 0 | 65535 | +| uint32 | uint32_t | 0 | 4294967295 | +| uint64 | uint64_t | 0 | 18446744073709551615 | +| int8 | int8_t | -128 | 127 | +| int16 | int16_t | -32768 | 32767 | +| int32 | int32_t | -2147483648 | 2147483647 | +| int64 | int64_t | -9223372036854775808 | 9223372036854775807 | + + +Usage examples: + +```lua +local x = ctypes.uint64(1234) +print(x) --> 1234ULL (cdata) +local y = ctypes.uint64("18446744073709551615") +print(y) --> 18446744073709551615ULL (cdata) +print(x + y) --> 1233ULL + +local z = tonumber(x) +print(z) --> 1234 (number) +``` diff --git a/doc/ru/scripting.md b/doc/ru/scripting.md index db3fcd921..1e6d73098 100644 --- a/doc/ru/scripting.md +++ b/doc/ru/scripting.md @@ -16,6 +16,7 @@ - [block](scripting/builtins/libblock.md) - [byteutil](scripting/builtins/libbyteutil.md) - [cameras](scripting/builtins/libcameras.md) + - [ctypes](scripting/builtins/libctypes.md) - [entities](scripting/builtins/libentities.md) - [file](scripting/builtins/libfile.md) - [gfx.blockwraps](scripting/builtins/libgfx-blockwraps.md) diff --git a/doc/ru/scripting/builtins/libctypes.md b/doc/ru/scripting/builtins/libctypes.md new file mode 100644 index 000000000..d6c985b44 --- /dev/null +++ b/doc/ru/scripting/builtins/libctypes.md @@ -0,0 +1,28 @@ +# Библиотека *ctypes* + +Библиотека для безопасной работы с типами языка С. + +| ctypes.\ | С-тип | Минимум | Максимум | +| ------------- | -------- | -------------------- | -------------------- | +| uint8 | uint8_t | 0 | 255 | +| uint16 | uint16_t | 0 | 65535 | +| uint32 | uint32_t | 0 | 4294967295 | +| uint64 | uint64_t | 0 | 18446744073709551615 | +| int8 | int8_t | -128 | 127 | +| int16 | int16_t | -32768 | 32767 | +| int32 | int32_t | -2147483648 | 2147483647 | +| int64 | int64_t | -9223372036854775808 | 9223372036854775807 | + + +Примеры использования: + +```lua +local x = ctypes.uint64(1234) +print(x) --> 1234ULL (cdata) +local y = ctypes.uint64("18446744073709551615") +print(y) --> 18446744073709551615ULL (cdata) +print(x + y) --> 1233ULL + +local z = tonumber(x) +print(z) --> 1234 (number) +``` From f082d7608dac0fc9a6ebb4bef7a24a9c6ef7cd2d Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 5 Aug 2026 20:23:46 +0300 Subject: [PATCH 152/217] fix black screen in water --- src/graphics/render/WorldRenderer.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/graphics/render/WorldRenderer.cpp b/src/graphics/render/WorldRenderer.cpp index 57cc90f24..4e2531d95 100644 --- a/src/graphics/render/WorldRenderer.cpp +++ b/src/graphics/render/WorldRenderer.cpp @@ -470,11 +470,16 @@ void WorldRenderer::renderFrame( setupWorldShader(entityShader, hudcam, engine.getSettings(), 0.0f); hands->render(camera); } - renderBlockOverlay(pctx); - + { + auto bctx = pctx.sub(); + bctx.useTexture(advanced_pipeline::TARGET_COLOR, nullptr); + renderBlockOverlay(pctx); + } glActiveTexture(GL_TEXTURE0); } +#include + void WorldRenderer::renderBlockOverlay(const DrawContext& wctx) { int x = std::floor(player.currentCamera->position.x); int y = std::floor(player.currentCamera->position.y); From 103bcb3751584fb25c19f6867485051231cf74f5 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Wed, 5 Aug 2026 20:24:32 +0300 Subject: [PATCH 153/217] remove trash --- src/graphics/render/WorldRenderer.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/graphics/render/WorldRenderer.cpp b/src/graphics/render/WorldRenderer.cpp index 4e2531d95..a611c7ab6 100644 --- a/src/graphics/render/WorldRenderer.cpp +++ b/src/graphics/render/WorldRenderer.cpp @@ -478,8 +478,6 @@ void WorldRenderer::renderFrame( glActiveTexture(GL_TEXTURE0); } -#include - void WorldRenderer::renderBlockOverlay(const DrawContext& wctx) { int x = std::floor(player.currentCamera->position.x); int y = std::floor(player.currentCamera->position.y); From c445f1a68f9bc1c9c9877cd91d010a1dd5240f0d Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 6 Aug 2026 18:20:53 +0300 Subject: [PATCH 154/217] fix zIndex type in docs --- doc/en/scripting/ui.md | 2 +- doc/ru/scripting/ui.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/en/scripting/ui.md b/doc/en/scripting/ui.md index 2d338a5aa..7a11b2f5e 100644 --- a/doc/en/scripting/ui.md +++ b/doc/en/scripting/ui.md @@ -53,7 +53,7 @@ Properties that apply to all elements: | contentOffset | vec2 | yes | *no* | element content offset | | cursor | string | yes | yes | cursor displayed on hover | | parent | Element | yes | *no* | parent element or nil | -| zIndex | bool | yes | *no* | z-index value. In panel, it determines the order | +| zIndex | int | yes | *no* | z-index value. In panel, it determines the order | Common element methods: diff --git a/doc/ru/scripting/ui.md b/doc/ru/scripting/ui.md index 631d5e00f..09e296f9b 100644 --- a/doc/ru/scripting/ui.md +++ b/doc/ru/scripting/ui.md @@ -53,7 +53,7 @@ document["worlds-panel"]:clear() | contentOffset | vec2 | да | *нет* | смещение содержимого | | cursor | string | да | да | курсор, отображаемый при наведении | | parent | Element | да | *нет* | родительский элемент или nil | -| zIndex | bool | да | *нет* | значение z-индекса. В panel определяет порядок | +| zIndex | int | да | *нет* | значение z-индекса. В panel определяет порядок | Общие методы элементов: From 1354e301c712a1d541ea1ec5215edd37579c0c13 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 6 Aug 2026 21:02:47 +0300 Subject: [PATCH 155/217] fix build warning --- src/logic/EngineController.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/logic/EngineController.cpp b/src/logic/EngineController.cpp index ec9ef0b2a..388fecb6f 100644 --- a/src/logic/EngineController.cpp +++ b/src/logic/EngineController.cpp @@ -283,7 +283,7 @@ void EngineController::createWorld( // generator-based environments must die in 1.0 bool genBasedEnv = false; - if (auto genDef = generators.find(environment)) { + if (generators.find(environment)) { genBasedEnv = true; } From 9ea50148b8a02a74befbcbc618a6a7dca7643316 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Thu, 6 Aug 2026 22:26:25 +0300 Subject: [PATCH 156/217] fix double click behaviour --- src/graphics/ui/GUI.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graphics/ui/GUI.cpp b/src/graphics/ui/GUI.cpp index 8f0f60520..6169a2578 100644 --- a/src/graphics/ui/GUI.cpp +++ b/src/graphics/ui/GUI.cpp @@ -188,8 +188,8 @@ void GUI::actMouse(Frame& frame, float delta, const CursorState& cursor) { doubleClicked = true; } else { pressed->click(cursorPos.x, cursorPos.y); + doubleClickTimer = 0.0f; } - doubleClickTimer = 0.0f; if (focus && focus != pressed) { focus->defocus(); } From 2448869bf922a5773dbfd420a0a0e2cb151b0712 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 7 Aug 2026 00:02:39 +0300 Subject: [PATCH 157/217] fix horizontal panel behaviour --- src/graphics/ui/elements/Panel.cpp | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/graphics/ui/elements/Panel.cpp b/src/graphics/ui/elements/Panel.cpp index 885ae3d61..87796a50c 100644 --- a/src/graphics/ui/elements/Panel.cpp +++ b/src/graphics/ui/elements/Panel.cpp @@ -97,16 +97,23 @@ void Panel::refresh() { } else { float maxh = size.y; for (auto& node : nodes) { - glm::vec2 nodesize = node->getSize(); const glm::vec4 margin = node->getMargin(); x += margin.x; - node->setPos(glm::vec2(x, y + margin.y)); - x += nodesize.x + margin.z + interval; - node->refresh(); - maxh = fmax( - maxh, y + margin.y + node->getSize().y + margin.w + padding.w + float ey = y + margin.y; + node->setPos(glm::vec2(x, ey)); + + int height = glm::floor( + size.y - padding.y - padding.w - margin.y - margin.w ); + if (node->isResizing()) { + node->setMaxSize({node->getMaxSize().x, height}); + node->setSize(glm::vec2(node->getSize().x, height)); + } + node->refresh(); + glm::vec2 nodesize = node->getSize(); + x += nodesize.x + margin.z + interval; + maxh = fmax(maxh, ey + nodesize.y + margin.w + padding.w); } actualLength = size.y; } From 863cc923a00ee2a6b5eadf38949b3f6803047a18 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 7 Aug 2026 22:26:58 +0300 Subject: [PATCH 158/217] feat: horizontal scroll --- src/graphics/ui/elements/Container.cpp | 25 ++++++++++++++----------- src/graphics/ui/elements/Container.hpp | 8 +++++--- src/graphics/ui/elements/Panel.cpp | 10 ++++++---- src/graphics/ui/elements/TextBox.cpp | 2 +- 4 files changed, 26 insertions(+), 19 deletions(-) diff --git a/src/graphics/ui/elements/Container.cpp b/src/graphics/ui/elements/Container.cpp index c869e9b3b..954c9879d 100644 --- a/src/graphics/ui/elements/Container.cpp +++ b/src/graphics/ui/elements/Container.cpp @@ -11,7 +11,7 @@ using namespace gui; Container::Container(GUI& gui, glm::vec2 size) : UINode(gui, size) { - actualLength = size.y; + actualLengthY = size.y; setColor(glm::vec4()); } @@ -26,7 +26,7 @@ std::shared_ptr Container::getAt(const glm::vec2& pos) { if (!isInside(pos)) { return nullptr; } - int diff = (actualLength-size.y); + int diff = (actualLengthY-size.y); if (scrollable && diff > 0 && pos.x > calcPos().x + getSize().x - scrollBarWidth) { return UINode::getAt(pos); } @@ -65,11 +65,11 @@ void Container::mouseMove(int x, int y) { auto pos = calcPos(); x -= pos.x; y -= pos.y; - int diff = (actualLength-size.y); + int diff = (actualLengthY-size.y); if (diff > 0) { - scroll -= (y - prevScrollY) / static_cast(size.y) * actualLength; - scroll = -glm::min( - glm::max(static_cast(-scroll), 0.0f), actualLength - size.y + scrollY -= (y - prevScrollY) / static_cast(size.y) * actualLengthY; + scrollY = -glm::min( + glm::max(static_cast(-scrollY), 0.0f), actualLengthY - size.y ); } prevScrollY = y; @@ -110,7 +110,10 @@ void Container::act(float delta) { } void Container::scrolled(int value) { - int diff = (actualLength-getSize().y); + auto size = getSize(); + int diff = (actualLengthY > size.y ? actualLengthY - size.y : actualLengthX - size.x); + int& scroll = (actualLengthY > size.y ? scrollY : scrollX); + if (scroll < 0 && diff <= 0) { scroll = 0; } @@ -146,14 +149,14 @@ void Container::draw(const DrawContext& pctx, const Assets& assets) { node->draw(pctx, assets); } - int diff = (actualLength-size.y); + int diff = (actualLengthY-size.y); if (scrollable && diff > 0) { - int h = glm::max(size.y / actualLength * size.y, scrollBarWidth / 2.0f); + int h = glm::max(size.y / actualLengthY * size.y, scrollBarWidth / 2.0f); batch->untexture(); batch->setColor(glm::vec4(1, 1, 1, 0.3f)); batch->rect( pos.x + size.x - scrollBarWidth, - pos.y - scroll / static_cast(diff) * (size.y - h), + pos.y - scrollY / static_cast(diff) * (size.y - h), scrollBarWidth, h ); } @@ -248,7 +251,7 @@ void Container::refresh() { } void Container::setScroll(int scroll) { - this->scroll = scroll; + this->scrollY = scroll; } const std::vector>& Container::getNodes() const { diff --git a/src/graphics/ui/elements/Container.hpp b/src/graphics/ui/elements/Container.hpp index 3ede2c2a1..36c366010 100644 --- a/src/graphics/ui/elements/Container.hpp +++ b/src/graphics/ui/elements/Container.hpp @@ -13,10 +13,12 @@ namespace gui { protected: std::vector> nodes; std::vector intervalEvents; - int scroll = 0; + int scrollY = 0; + int scrollX = 0; int scrollStep = 40; int scrollBarWidth = 10; - int actualLength = 0; + int actualLengthY = 0; + int actualLengthX = 0; bool scrollable = true; bool isScrolling() const { @@ -40,7 +42,7 @@ namespace gui { virtual void setScrollable(bool flag); void listenInterval(float interval, OnTimeOut callback, int repeat=-1); virtual glm::vec2 getContentOffset() const override { - return glm::vec2(0.0f, scroll); + return glm::vec2(scrollX, scrollY); }; virtual void setSize(const glm::vec2& size) override; virtual int getScrollStep() const; diff --git a/src/graphics/ui/elements/Panel.cpp b/src/graphics/ui/elements/Panel.cpp index 87796a50c..3b452e892 100644 --- a/src/graphics/ui/elements/Panel.cpp +++ b/src/graphics/ui/elements/Panel.cpp @@ -41,10 +41,10 @@ glm::vec2 Panel::getContentSize() const { void Panel::cropToContent() { if (maxLength > 0.0f) { setSize(glm::vec2( - getSize().x, glm::max(minLength, glm::min(maxLength, actualLength)) + glm::max(minLength, glm::min(maxLength, actualLengthX)), glm::max(minLength, glm::min(maxLength, actualLengthY)) )); } else { - setSize(glm::vec2(getSize().x, glm::max(minLength, actualLength))); + setSize(glm::vec2(glm::max(minLength, actualLengthX), glm::max(minLength, actualLengthY))); } } @@ -93,7 +93,8 @@ void Panel::refresh() { y += nodeSize.y + margin.w + interval; maxw = fmax(maxw, ex + nodeSize.x + margin.z + padding.z); } - actualLength = y + padding.w; + actualLengthX = size.x; + actualLengthY = y + padding.w; } else { float maxh = size.y; for (auto& node : nodes) { @@ -115,6 +116,7 @@ void Panel::refresh() { x += nodesize.x + margin.z + interval; maxh = fmax(maxh, ey + nodesize.y + margin.w + padding.w); } - actualLength = size.y; + actualLengthY = size.y; + actualLengthX = x + padding.z; } } diff --git a/src/graphics/ui/elements/TextBox.cpp b/src/graphics/ui/elements/TextBox.cpp index f37673fc1..7bd7cd646 100644 --- a/src/graphics/ui/elements/TextBox.cpp +++ b/src/graphics/ui/elements/TextBox.cpp @@ -452,7 +452,7 @@ void TextBox::refreshLabel() { uint height = label->getLinesNumber() * rawTextCache.metrics.lineHeight * label->getLineInterval(); label->setSize(glm::vec2(label->getSize().x, height)); - actualLength = height; + actualLengthY = height; } else { setScrollable(false); } From 660bd0a5d398bfad225ce5428cc19f2f5d0169b6 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 7 Aug 2026 22:28:48 +0300 Subject: [PATCH 159/217] format Container.cpp and Panel.cpp --- src/graphics/ui/elements/Container.cpp | 89 +++++++++++++++----------- src/graphics/ui/elements/Panel.cpp | 8 ++- 2 files changed, 58 insertions(+), 39 deletions(-) diff --git a/src/graphics/ui/elements/Container.cpp b/src/graphics/ui/elements/Container.cpp index 954c9879d..3d03c2fe3 100644 --- a/src/graphics/ui/elements/Container.cpp +++ b/src/graphics/ui/elements/Container.cpp @@ -1,9 +1,9 @@ #include "Container.hpp" -#include "graphics/core/DrawContext.hpp" -#include "graphics/core/Batch2D.hpp" -#include "window/Window.hpp" #include "../GUI.hpp" +#include "graphics/core/Batch2D.hpp" +#include "graphics/core/DrawContext.hpp" +#include "window/Window.hpp" #include #include @@ -26,15 +26,15 @@ std::shared_ptr Container::getAt(const glm::vec2& pos) { if (!isInside(pos)) { return nullptr; } - int diff = (actualLengthY-size.y); - if (scrollable && diff > 0 && pos.x > calcPos().x + getSize().x - scrollBarWidth) { + int diff = (actualLengthY - size.y); + if (scrollable && diff > 0 && + pos.x > calcPos().x + getSize().x - scrollBarWidth) { return UINode::getAt(pos); } - for (int i = nodes.size()-1; i >= 0; i--) { + for (int i = nodes.size() - 1; i >= 0; i--) { auto& node = nodes[i]; - if (!node->isVisible()) - continue; + if (!node->isVisible()) continue; auto hover = node->getAt(pos); if (hover != nullptr) { return hover; @@ -45,7 +45,7 @@ std::shared_ptr Container::getAt(const glm::vec2& pos) { void Container::click(int x, int y) { UINode::click(x, y); - + if (scrollable) { auto pos = calcPos(); x -= pos.x; @@ -65,9 +65,10 @@ void Container::mouseMove(int x, int y) { auto pos = calcPos(); x -= pos.x; y -= pos.y; - int diff = (actualLengthY-size.y); + int diff = (actualLengthY - size.y); if (diff > 0) { - scrollY -= (y - prevScrollY) / static_cast(size.y) * actualLengthY; + scrollY -= + (y - prevScrollY) / static_cast(size.y) * actualLengthY; scrollY = -glm::min( glm::max(static_cast(-scrollY), 0.0f), actualLengthY - size.y ); @@ -101,26 +102,28 @@ void Container::act(float delta) { } } } - intervalEvents.erase(std::remove_if( - intervalEvents.begin(), intervalEvents.end(), - [](const IntervalEvent& event) { - return event.repeat == 0; - } - ), intervalEvents.end()); + intervalEvents.erase( + std::remove_if( + intervalEvents.begin(), + intervalEvents.end(), + [](const IntervalEvent& event) { return event.repeat == 0; } + ), + intervalEvents.end() + ); } void Container::scrolled(int value) { auto size = getSize(); - int diff = (actualLengthY > size.y ? actualLengthY - size.y : actualLengthX - size.x); - int& scroll = (actualLengthY > size.y ? scrollY : scrollX); + bool isVertical = actualLengthY > size.y; + int diff = (isVertical ? actualLengthY - size.y : actualLengthX - size.x); + int& scroll = (isVertical ? scrollY : scrollX); if (scroll < 0 && diff <= 0) { scroll = 0; } if (diff > 0 && scrollable) { scroll += value * scrollStep; - if (scroll > 0) - scroll = 0; + if (scroll > 0) scroll = 0; if (-scroll > diff) { scroll = -diff; } @@ -143,21 +146,25 @@ void Container::draw(const DrawContext& pctx, const Assets& assets) { if (!nodes.empty()) { batch->flush(); DrawContext ctx = pctx.sub(); - ctx.setScissors(glm::vec4(pos.x, pos.y, glm::ceil(size.x), glm::ceil(size.y))); + ctx.setScissors( + glm::vec4(pos.x, pos.y, glm::ceil(size.x), glm::ceil(size.y)) + ); for (const auto& node : nodes) { - if (node->isVisible()) - node->draw(pctx, assets); + if (node->isVisible()) node->draw(pctx, assets); } - int diff = (actualLengthY-size.y); + int diff = (actualLengthY - size.y); if (scrollable && diff > 0) { - int h = glm::max(size.y / actualLengthY * size.y, scrollBarWidth / 2.0f); + int h = glm::max( + size.y / actualLengthY * size.y, scrollBarWidth / 2.0f + ); batch->untexture(); batch->setColor(glm::vec4(1, 1, 1, 0.3f)); batch->rect( pos.x + size.x - scrollBarWidth, pos.y - scrollY / static_cast(diff) * (size.y - h), - scrollBarWidth, h + scrollBarWidth, + h ); } batch->flush(); @@ -166,8 +173,7 @@ void Container::draw(const DrawContext& pctx, const Assets& assets) { void Container::drawBackground(const DrawContext& pctx, const Assets&) { glm::vec4 color = calcColor(); - if (color.a <= 0.001f) - return; + if (color.a <= 0.001f) return; glm::vec2 pos = calcPos(); auto batch = pctx.getBatch2D(); @@ -197,11 +203,16 @@ void Container::add(const std::shared_ptr& node, glm::vec2 pos) { void Container::remove(UINode* selected) { selected->setParent(nullptr); - nodes.erase(std::remove_if(nodes.begin(), nodes.end(), - [selected](const std::shared_ptr& node) { - return node.get() == selected; - } - ), nodes.end()); + nodes.erase( + std::remove_if( + nodes.begin(), + nodes.end(), + [selected](const std::shared_ptr& node) { + return node.get() == selected; + } + ), + nodes.end() + ); refresh(); } @@ -245,9 +256,13 @@ void Container::setScrollStep(int step) { } void Container::refresh() { - std::stable_sort(nodes.begin(), nodes.end(), [](const auto& a, const auto& b) { - return a->getZIndex() < b->getZIndex(); - }); + std::stable_sort( + nodes.begin(), + nodes.end(), + [](const auto& a, const auto& b) { + return a->getZIndex() < b->getZIndex(); + } + ); } void Container::setScroll(int scroll) { diff --git a/src/graphics/ui/elements/Panel.cpp b/src/graphics/ui/elements/Panel.cpp index 3b452e892..4d133c875 100644 --- a/src/graphics/ui/elements/Panel.cpp +++ b/src/graphics/ui/elements/Panel.cpp @@ -41,10 +41,14 @@ glm::vec2 Panel::getContentSize() const { void Panel::cropToContent() { if (maxLength > 0.0f) { setSize(glm::vec2( - glm::max(minLength, glm::min(maxLength, actualLengthX)), glm::max(minLength, glm::min(maxLength, actualLengthY)) + glm::max(minLength, glm::min(maxLength, actualLengthX)), + glm::max(minLength, glm::min(maxLength, actualLengthY)) )); } else { - setSize(glm::vec2(glm::max(minLength, actualLengthX), glm::max(minLength, actualLengthY))); + setSize(glm::vec2( + glm::max(minLength, actualLengthX), + glm::max(minLength, actualLengthY) + )); } } From 8c5a549773468d3372697d84bc1fee901df395c1 Mon Sep 17 00:00:00 2001 From: MihailRis Date: Fri, 7 Aug 2026 22:39:48 +0300 Subject: [PATCH 160/217] cleanup element classes --- src/graphics/ui/elements/CheckBox.hpp | 29 +++-- src/graphics/ui/elements/Frame.hpp | 2 +- src/graphics/ui/elements/InventoryView.hpp | 10 +- src/graphics/ui/elements/Label.hpp | 54 ++++----- src/graphics/ui/elements/Menu.hpp | 2 +- src/graphics/ui/elements/ModelViewer.hpp | 7 +- src/graphics/ui/elements/Plotter.hpp | 2 +- src/graphics/ui/elements/TextBox.cpp | 8 +- src/graphics/ui/elements/TextBox.hpp | 124 ++++++++++----------- src/graphics/ui/elements/TrackBar.cpp | 2 +- src/graphics/ui/elements/TrackBar.hpp | 42 +++---- 11 files changed, 141 insertions(+), 141 deletions(-) diff --git a/src/graphics/ui/elements/CheckBox.hpp b/src/graphics/ui/elements/CheckBox.hpp index 5ca144618..0f520d43f 100644 --- a/src/graphics/ui/elements/CheckBox.hpp +++ b/src/graphics/ui/elements/CheckBox.hpp @@ -6,7 +6,7 @@ #include "Panel.hpp" namespace gui { - class CheckBox : public UINode { + class CheckBox final : public UINode { protected: glm::vec4 checkColor {1.0f, 1.0f, 1.0f, 0.4f}; boolsupplier supplier = nullptr; @@ -15,23 +15,22 @@ namespace gui { public: explicit CheckBox(GUI& gui, bool checked = false); - virtual void draw(const DrawContext& pctx, const Assets& assets) - override; + void draw(const DrawContext& pctx, const Assets& assets) override; - virtual void mouseRelease(int x, int y) override; + void mouseRelease(int x, int y) override; - virtual void setSupplier(boolsupplier supplier); - virtual void setConsumer(boolconsumer consumer); + void setSupplier(boolsupplier supplier); + void setConsumer(boolconsumer consumer); - virtual CheckBox* setChecked(bool flag); + CheckBox* setChecked(bool flag); - virtual bool isChecked() const { + bool isChecked() const { if (supplier) return supplier(); return checked; } }; - class FullCheckBox : public Panel { + class FullCheckBox final : public Panel { protected: std::shared_ptr checkbox; std::shared_ptr