diff --git a/dev/tests/attachments/line_clipping_test.png b/dev/tests/attachments/line_clipping_test.png new file mode 100644 index 000000000..bb20d1841 Binary files /dev/null and b/dev/tests/attachments/line_clipping_test.png differ diff --git a/dev/tests/canvas_lines.lua b/dev/tests/canvas_lines.lua new file mode 100644 index 000000000..c7ef68520 --- /dev/null +++ b/dev/tests/canvas_lines.lua @@ -0,0 +1,51 @@ +local W, H = 128, 128 + +local canvas = Canvas({W, H}) +canvas:clear(0xff202020) + +local WHITE = 0xffffffff +local RED = 0xffff4040 +local GREEN = 0xff40ff40 +local BLUE = 0xff4080ff +local YELLOW = 0xffffff40 + +-- border +canvas:line(0, 0, W - 1, 0, WHITE) +canvas:line(W - 1, 0, W - 1, H - 1, WHITE) +canvas:line(W - 1, H - 1, 0, H - 1, WHITE) +canvas:line(0, H - 1, 0, 0, WHITE) + +-- horizontal +canvas:line(-40, 16, W + 40, 16, RED) +canvas:line(-40, H / 2, W + 40, H / 2, RED) +canvas:line(-40, H - 17, W + 40, H - 17, RED) + +-- vertical +canvas:line(16, -40, 16, H + 40, GREEN) +canvas:line(W / 2, -40, W / 2, H + 40, GREEN) +canvas:line(W - 17, -40, W - 17, H + 40, GREEN) + +-- diagonals +canvas:line(-40, -40, W + 40, H + 40, BLUE) +canvas:line(-40, H + 40, W + 40, -40, BLUE) + +canvas:line(W / 2, -40, W + 40, H / 2, BLUE) +canvas:line(-40, H / 2, W / 2, H + 40, BLUE) + +-- outer +canvas:line(-30, -30, 20, 20, YELLOW) +canvas:line(W - 20, H - 20, W + 30, H + 30, YELLOW) + +canvas:line(W + 30, -30, W - 20, 20, YELLOW) +canvas:line(-30, H + 30, 20, H - 20, YELLOW) + +-- completely outside +canvas:line(-50, -20, -10, H + 20, 0xffff00ff) +canvas:line(W + 10, -20, W + 50, H + 20, 0xffff00ff) +canvas:line(-20, -20, W + 20, -10, 0xffff00ff) +canvas:line(-20, H + 10, W + 20, H + 30, 0xffff00ff) + +local expected = Canvas.decode( + file.read_bytes("script:attachments/line_clipping_test.png"), "png" +) +asserts.equals(expected, canvas) diff --git a/doc/en/scripting/builtins/libinventory.md b/doc/en/scripting/builtins/libinventory.md index 8cf2d7c3e..172ec63e6 100644 --- a/doc/en/scripting/builtins/libinventory.md +++ b/doc/en/scripting/builtins/libinventory.md @@ -162,7 +162,19 @@ inventory.get_all_data( invid: int, -- slot index slot: int, -) -> table +) -> table | nil + +-- Sets the values ​​of all local item properties. +inventory.set_all_data( + -- inventory id + invid: int, + -- slot index + slot: int, + -- property:value table + fields: table, + -- clear other property values + [optional] clear: boolean = false +) ``` ```lua diff --git a/doc/en/scripting/ecs.md b/doc/en/scripting/ecs.md index 878452928..52be70363 100644 --- a/doc/en/scripting/ecs.md +++ b/doc/en/scripting/ecs.md @@ -264,3 +264,9 @@ function on_used(playerid: int) ``` Called when an entity is used (RMB by entity). The player ID is passed as an argument. + +```lua +function on_player_set(playerid: int) +``` + +Called when an entity is attached to a player. diff --git a/doc/ru/scripting/builtins/libinventory.md b/doc/ru/scripting/builtins/libinventory.md index 50ed8dccb..4a836f83b 100644 --- a/doc/ru/scripting/builtins/libinventory.md +++ b/doc/ru/scripting/builtins/libinventory.md @@ -170,7 +170,19 @@ inventory.get_all_data( invid: int, -- индекс слота slot: int, -) -> table +) -> table | nil + +-- Устанавливает значения локальных cвойств предмета. +inventory.set_all_data( + -- id инвентаря + invid: int, + -- индекс слота + slot: int, + -- таблица cвойство:значение + fields: table, + -- удалить остальные значения свойств + [опционально] clear: boolean = false +) ``` ```lua @@ -222,4 +234,4 @@ inventory.use( -- индекс слота slot: int ) -``` \ No newline at end of file +``` diff --git a/doc/ru/scripting/ecs.md b/doc/ru/scripting/ecs.md index 1a5f6ccf3..d659f342d 100644 --- a/doc/ru/scripting/ecs.md +++ b/doc/ru/scripting/ecs.md @@ -265,3 +265,8 @@ function on_used(playerid: int) Вызывается при использовании сущности (ПКМ по сущности). ID игрока передается в качестве аргумента. +```lua +function on_player_set(playerid: int) +``` + +Вызывается при прикреплении сущности к игроку. diff --git a/res/modules/internal/asserts.lua b/res/modules/internal/asserts.lua index 2317bb7ce..b84d5d37f 100644 --- a/res/modules/internal/asserts.lua +++ b/res/modules/internal/asserts.lua @@ -1,8 +1,77 @@ local this = {} +local app = __vc_app -function this.equals(expected, fact) +local function diff_canvas(expected, fact) + this.equals(expected.width, fact.width, "image width") + this.equals(expected.width, fact.width, "image height") + + local diff = Canvas({expected.width, expected.height}) + + local mismatches = 0 + + for y = 0, expected.height - 1 do + for x = 0, expected.width - 1 do + local ca = expected:at(x, y) + local cb = fact:at(x, y) + + if ca == cb then + diff:set(x, y, ca) + else + diff:set(x, y, 0xff0000ff) + mismatches = mismatches + 1 + end + end + end + + return diff, mismatches +end + +local TEST_ARTIFACTS_PATH = "export:test-results" + +local function get_test_artifact_path(name) + local path = TEST_ARTIFACTS_PATH + if not file.isdir(path) then + file.mkdirs(path) + end + return file.join(path, name) +end + +function this.equals(expected, fact, prefix) + local mt = getmetatable(expected) + if mt == Canvas then + local diff, mismatches = diff_canvas(expected, fact) + if mismatches > 0 then + prefix = prefix or app.script or base64.encode_urlsafe(random.bytes(6)) + local path_diff = get_test_artifact_path( + string.format("%s.diff.png", prefix) + ) + local path_fact = get_test_artifact_path( + string.format("%s.fact.png", prefix) + ) + local path_expected = get_test_artifact_path( + string.format("%s.expected.png", prefix) + ) + file.write_bytes(path_diff, diff:encode('png')) + file.write_bytes(path_fact, fact:encode('png')) + file.write_bytes(path_expected, expected:encode('png')) + local total_pixels = expected.width * expected.height + assert(mismatches == 0, string.format( + "%s pixels of %s (%s%%) are different, diff saved as %s", + mismatches, + total_pixels, + math.round(mismatches / total_pixels * 100, 2), + string.escape(path_diff)) + ) + end + return + end + if prefix then + prefix = prefix .. " " + else + prefix = "" + end assert(fact == expected, string.format( - "(fact == expected) assertion failed\n Expected: %s\n Fact: %s", + prefix .. "(fact == expected) assertion failed\n Expected: %s\n Fact: %s", expected, fact )) end diff --git a/res/modules/internal/bytearray.lua b/res/modules/internal/bytearray.lua index 739bb7cea..58b33a520 100644 --- a/res/modules/internal/bytearray.lua +++ b/res/modules/internal/bytearray.lua @@ -12,11 +12,30 @@ FFI.cdef[[ } bytearray_t; ]] -local malloc = FFI.C.malloc local free = FFI.C.free + local FFIBytearray local bytearray_type +local allocated_bytes = 0 +local GC_THRESHOLD = 50 * 1024 * 1024 + +local function malloc(size) + local raw = FFI.C.malloc(size) + if raw == nil then + return nil + end + + allocated_bytes = allocated_bytes + size + + if allocated_bytes >= GC_THRESHOLD then + collectgarbage("step", 200) + allocated_bytes = 0 + end + + return raw +end + local function grow_buffer(self, elems) local new_capacity = math.ceil(self.capacity / 0.75 + elems) local prev = self.bytes diff --git a/res/scripts/components/mob.lua b/res/scripts/components/mob.lua index 92c2a3723..9f2dc53b7 100644 --- a/res/scripts/components/mob.lua +++ b/res/scripts/components/mob.lua @@ -142,14 +142,16 @@ end function is_flight() return flight end -function set_flight(flag) flight = flag end +function set_flight(flag) + flight = flag + body:set_gravity_scale(flight and 0.0 or props.gravity_scale) +end local prev_angle = (vec2.angle({dir[3], dir[1]})) % 360 function on_physics_update(delta) local grounded = body:is_grounded() body:set_vdamping(flight) - body:set_gravity_scale(flight and 0.0 or props.gravity_scale) body:set_linear_damping( (flight or not grounded) and props.air_damping or props.ground_damping ) diff --git a/res/scripts/components/player.lua b/res/scripts/components/player.lua index 4f2ec53a8..e0f8cb900 100644 --- a/res/scripts/components/player.lua +++ b/res/scripts/components/player.lua @@ -58,16 +58,12 @@ local function process_player_inputs(pid, rot, delta) end end -local prepared = false -body:set_enabled(false) -mob.set_flight(true) +function on_player_set(pid) + mob.set_flight(player.is_flight(pid)) + body:set_body_type(player.is_noclip(pid) and "kinematic" or "dynamic") +end function on_physics_update(delta) - if not prepared then - prepared = true - body:set_enabled(true) - end - local pid = entity:get_player() if pid == -1 then return diff --git a/src/coders/png.cpp b/src/coders/png.cpp index 030844352..18cc0a474 100644 --- a/src/coders/png.cpp +++ b/src/coders/png.cpp @@ -49,9 +49,9 @@ static util::Buffer write_to_memory(uint width, uint height, const ubyte* png_write_info(png_ptr, info_ptr); auto row = std::make_unique(pixsize * width); - for (uint y = 0; y < height; y++) { - for (uint x = 0; x < width; x++) { - for (uint i = 0; i < pixsize; i++) { + for (int y = height-1; y >= 0; y--) { + for (int x = 0; x < width; x++) { + for (int i = 0; i < pixsize; i++) { row[x * pixsize + i] = (png_byte)data[(y * width + x) * pixsize + i]; } diff --git a/src/graphics/core/ImageData.cpp b/src/graphics/core/ImageData.cpp index 6875d7ab9..5f7761188 100644 --- a/src/graphics/core/ImageData.cpp +++ b/src/graphics/core/ImageData.cpp @@ -1,5 +1,7 @@ #include "ImageData.hpp" +#include "debug/Logger.hpp" + #include #include #include @@ -7,6 +9,8 @@ #include #include +static debug::Logger logger("image-data"); + ImageData::ImageData(ImageFormat format, uint width, uint height) : format(format), width(width), height(height) { size_t pixsize; @@ -106,47 +110,83 @@ std::unique_ptr ImageData::cropped(int x, int y, int width, int heigh return subImage; } -static bool clip_line(int& x1, int& y1, int& x2, int& y2, int width, int height) { - const int left = 0; - const int right = width; - const int bottom = 0; - const int top = height; +static bool clip_line( + int& x0, int& y0, int& x1, int& y1, int width, int height +) { + if (width <= 0 || height <= 0) { + return false; + } + constexpr int INSIDE = 0; + constexpr int LEFT = 1; + constexpr int RIGHT = 2; + constexpr int BOTTOM = 4; + constexpr int TOP = 8; - int dx = x2 - x1; - int dy = y2 - y1; + const int xmin = 0; + const int ymin = 0; + const int xmax = width - 1; + const int ymax = height - 1; - float t0 = 0.0f; - float t1 = 1.0f; + auto outcode = [&](int x, int y) { + int code = INSIDE; - auto clip = [](int p, int q, float& t0, float& t1) { - if (p == 0) { - return q >= 0; - } - float t = static_cast(q) / p; - if (p < 0) { - if (t > t1) return false; - if (t > t0) t0 = t; - } else { - if (t < t0) return false; - if (t < t1) t1 = t; - } - return true; + if (x < xmin) + code |= LEFT; + else if (x > xmax) + code |= RIGHT; + + if (y < ymin) + code |= BOTTOM; + else if (y > ymax) + code |= TOP; + + return code; }; - if (!clip(-dx, x1 - left, t0, t1)) return false; - if (!clip( dx, right - x1, t0, t1)) return false; - if (!clip(-dy, y1 - bottom, t0, t1)) return false; - if (!clip( dy, top - y1, t0, t1)) return false; + int c0 = outcode(x0, y0); + int c1 = outcode(x1, y1); - if (t1 < 1.0f) { - x2 = x1 + static_cast(std::round(t1 * dx)); - y2 = y1 + static_cast(std::round(t1 * dy)); + while (true) { + if (!(c0 | c1)) { + return true; + } + if (c0 & c1) { + return false; + } + + const int out = c0 ? c0 : c1; + + double x = 0.0; + double y = 0.0; + + if (out & TOP) { + y = ymax; + x = x0 + (x1 - x0) * static_cast(ymax - y0) / (y1 - y0); + } else if (out & BOTTOM) { + y = ymin; + x = x0 + (x1 - x0) * static_cast(ymin - y0) / (y1 - y0); + } else if (out & RIGHT) { + x = xmax; + y = y0 + (y1 - y0) * static_cast(xmax - x0) / (x1 - x0); + } else { // LEFT + x = xmin; + y = y0 + (y1 - y0) * static_cast(xmin - x0) / (x1 - x0); + } + + if (out == c0) { + x0 = std::clamp(static_cast(std::lround(x)), xmin, xmax); + y0 = std::clamp(static_cast(std::lround(y)), ymin, ymax); + c0 = outcode(x0, y0); + } else { + x1 = std::clamp(static_cast(std::lround(x)), xmin, xmax); + y1 = std::clamp(static_cast(std::lround(y)), ymin, ymax); + c1 = outcode(x1, y1); + } } - if (t0 > 0.0f) { - x1 = x1 + static_cast(std::round(t0 * dx)); - y1 = y1 + static_cast(std::round(t0 * dy)); - } - return true; +} + +static bool is_point_outside(int x, int y, int width, int height) { + return x < 0 || y < 0 || x >= width || y >= height; } template @@ -155,11 +195,24 @@ static void draw_line(ImageData& image, int x1, int y1, int x2, int y2, const gl uint width = image.getWidth(); uint height = image.getHeight(); + glm::ivec4 init {x1, y1, x2, y2}; + if ((x1 < 0 || x1 >= width || x2 < 0 || x2 >= width || y1 < 0 || y1 >= height || y2 < 0 || y2 >= height) && !clip_line(x1, y1, x2, y2, width, height)) { return; } + // spam info for bug report + if (is_point_outside(x1, y1, width, height) || is_point_outside(x2, y2, width, height)) { + logger.warning() << "clip_line fault: [" << init.x << ", " << init.y + << "] - [" << init.z << ", " << init.w << "] --> [" + << x1 << ", " << y1 << "] - [" << x2 << ", " << y2 + << "]"; + x1 = glm::clamp(x1, 0, static_cast(width) - 1); + y1 = glm::clamp(y1, 0, static_cast(height) - 1); + x2 = glm::clamp(x2, 0, static_cast(width) - 1); + y2 = glm::clamp(y2, 0, static_cast(height) - 1); + } int dx = std::abs(x2 - x1); int dy = -std::abs(y2 - y1); diff --git a/src/items/ItemStack.hpp b/src/items/ItemStack.hpp index 91f2d3e74..4896bc39e 100644 --- a/src/items/ItemStack.hpp +++ b/src/items/ItemStack.hpp @@ -55,6 +55,16 @@ public: return fields; } + void setFields(dv::value&& table, bool clear) { + if (clear) { + fields = std::move(table); + return; + } + for (auto&& [key, value] : table.asObject()) { + fields[std::move(key)] = std::move(value); + } + } + bool hasFields() const { return fields != nullptr; } diff --git a/src/logic/LevelController.cpp b/src/logic/LevelController.cpp index ae0fa6798..704949851 100644 --- a/src/logic/LevelController.cpp +++ b/src/logic/LevelController.cpp @@ -82,7 +82,6 @@ void LevelController::update(float delta, bool pause) { continue; } player->rotationInterpolation.updateTimer(delta); - player->updateEntity(); glm::vec3 position = player->getPosition(); player->chunks->configure( glm::floor(position.x), @@ -96,9 +95,9 @@ void LevelController::update(float delta, bool pause) { *player, player.get() == clientPlayer ); + player->updateEntity(); } if (!pause) { - // update all objects that needed blocks->update(delta, settings.chunks.padding.get()); level->entities->update(delta); for (const auto& [_, player] : *level->players) { diff --git a/src/logic/scripting/lua/libs/libinventory.cpp b/src/logic/scripting/lua/libs/libinventory.cpp index 3ec9d2ca3..2aea52394 100644 --- a/src/logic/scripting/lua/libs/libinventory.cpp +++ b/src/logic/scripting/lua/libs/libinventory.cpp @@ -237,6 +237,14 @@ static int l_get_all_data(lua::State* L, ItemStack& stack) { return lua::pushvalue(L, stack.getFields()); } +static int l_set_all_data(lua::State* L, ItemStack& stack) { + if (!lua::istable(L, 3)) { + throw std::runtime_error("table expected as argument 1"); + } + stack.setFields(lua::tovalue(L, 3), lua::toboolean(L, 4)); + return 0; +} + static int l_has_data(lua::State* L, ItemStack& stack) { auto key = lua::tostring(L, 3); if (key == nullptr) { @@ -267,6 +275,7 @@ const luaL_Reg inventorylib[] = { {"get_data", lua::wrap>}, {"set_data", lua::wrap>}, {"get_all_data", lua::wrap>}, + {"set_all_data", lua::wrap>}, {"has_data", lua::wrap>}, {"create", lua::wrap}, {"remove", lua::wrap}, diff --git a/src/logic/scripting/lua/usertypes/lua_type_canvas.cpp b/src/logic/scripting/lua/usertypes/lua_type_canvas.cpp index 37c53abd9..c670e23df 100644 --- a/src/logic/scripting/lua/usertypes/lua_type_canvas.cpp +++ b/src/logic/scripting/lua/usertypes/lua_type_canvas.cpp @@ -81,16 +81,17 @@ union RGBA { uint8_t arr[4]; uint32_t rgba; }; +static_assert(sizeof(RGBA) == 4); -static RGBA* get_at(const ImageData& data, uint index) { - if (index >= data.getWidth() * data.getHeight()) { - return nullptr; - } +static RGBA* get_at(const ImageData& data, size_t index) { return reinterpret_cast(data.getData() + index * sizeof(RGBA)); } static RGBA* get_at(const ImageData& data, uint x, uint y) { - return get_at(data, y * data.getWidth() + x); + if (x >= data.getWidth() || y >= data.getHeight()) { + return nullptr; + } + return get_at(data, static_cast(y) * data.getWidth() + x); } static RGBA* get_at(State* L, uint x, uint y) { diff --git a/src/logic/scripting/scripting.hpp b/src/logic/scripting/scripting.hpp index 9d29528b8..4c71a3aa6 100644 --- a/src/logic/scripting/scripting.hpp +++ b/src/logic/scripting/scripting.hpp @@ -142,6 +142,7 @@ namespace scripting { void on_entity_grounded(const Entity& entity, float force); void on_entity_fall(const Entity& entity); void on_entity_save(const Entity& entity); + void on_entity_player_set(const Entity& entity, int64_t pid); void on_entities_update(int tps, int parts, int part); void on_entities_physics_update(float delta); void on_entities_render(float delta); diff --git a/src/logic/scripting/scripting_entities.cpp b/src/logic/scripting/scripting_entities.cpp index 375b382a9..ca9ed6db1 100644 --- a/src/logic/scripting/scripting_entities.cpp +++ b/src/logic/scripting/scripting_entities.cpp @@ -71,6 +71,7 @@ static void init_component_events( funcsset.on_aim_off = lua::hasfield(L, "on_aim_off"); funcsset.on_attacked = lua::hasfield(L, "on_attacked"); funcsset.on_used = lua::hasfield(L, "on_used"); + funcsset.on_player_set = lua::hasfield(L, "on_player_set"); lua::pop(L); } @@ -238,6 +239,15 @@ void scripting::on_entity_save(const Entity& entity) { ); } +void scripting::on_entity_player_set(const Entity& entity, int64_t pid) { + process_entity_callback( + entity, + "on_player_set", + &EntityFuncsSet::on_player_set, + [pid](auto L) { return lua::pushinteger(L, pid); } + ); +} + void scripting::on_sensor_enter( const Entity& entity, size_t index, entityid_t oid ) { diff --git a/src/objects/Entity.cpp b/src/objects/Entity.cpp index 0bf898351..6629d2981 100644 --- a/src/objects/Entity.cpp +++ b/src/objects/Entity.cpp @@ -113,6 +113,11 @@ int64_t Entity::getPlayer() const { } void Entity::setPlayer(int64_t id) { - registry.get(entity).player = id; + auto& eid = registry.get(entity); + if (eid.player == id) { + return; + } + eid.player = id; + scripting::on_entity_player_set(*this, id); } diff --git a/src/objects/Player.cpp b/src/objects/Player.cpp index fec65d7de..c0fe61744 100644 --- a/src/objects/Player.cpp +++ b/src/objects/Player.cpp @@ -81,7 +81,7 @@ void Player::updateEntity() { } } -Hitbox* Player::getHitbox() { +Hitbox* Player::getHitbox() const { if (auto entity = level.entities->get(eid)) { return &entity->getRigidbody().hitbox; } diff --git a/src/objects/Player.hpp b/src/objects/Player.hpp index a095ccfb7..54ed70023 100644 --- a/src/objects/Player.hpp +++ b/src/objects/Player.hpp @@ -135,7 +135,7 @@ public: bool isCurrentCameraBuiltin() const; - Hitbox* getHitbox(); + Hitbox* getHitbox() const; void setSpawnPoint(glm::vec3 point); glm::vec3 getSpawnPoint() const; diff --git a/src/objects/ScriptComponents.hpp b/src/objects/ScriptComponents.hpp index 2711d4823..92c21d49f 100644 --- a/src/objects/ScriptComponents.hpp +++ b/src/objects/ScriptComponents.hpp @@ -17,6 +17,7 @@ struct EntityFuncsSet { bool on_aim_off; bool on_attacked; bool on_used; + bool on_player_set; }; struct UserComponent { diff --git a/src/objects/rigging.cpp b/src/objects/rigging.cpp index f686bfbcf..f47f7c733 100644 --- a/src/objects/rigging.cpp +++ b/src/objects/rigging.cpp @@ -88,12 +88,22 @@ void Skeleton::deserialize(const dv::value& root) { void Skeleton::setConfig(std::shared_ptr rigConfig) { config = std::move(rigConfig); + + const auto& bones = config->getBones(); + pose.matrices.resize( - config->getBones().size(), glm::mat4(1.0f) + bones.size(), glm::mat4(1.0f) ); calculated.matrices.resize( - config->getBones().size(), glm::mat4(1.0f) + bones.size(), glm::mat4(1.0f) ); + + modelOverrides.resize(bones.size()); + flags.resize(bones.size()); + + for (size_t i = 0; i < bones.size(); i++) { + flags[i].visible = true; + } } static void get_all_nodes(std::vector& nodes, Bone* node) { diff --git a/src/voxels/blocks_agent.cpp b/src/voxels/blocks_agent.cpp index 9d1f0cc17..d72ddcceb 100644 --- a/src/voxels/blocks_agent.cpp +++ b/src/voxels/blocks_agent.cpp @@ -309,7 +309,9 @@ static inline voxel* raycast_blocks( } else { iend = {ix, iy, iz}; norm = {0, 0, 0}; - norm[steppedIndex] = -step[steppedIndex]; + if (steppedIndex != -1) { + norm[steppedIndex] = -step[steppedIndex]; + } return voxel; } }