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/dev/tests/raycast.lua b/dev/tests/raycast.lua new file mode 100644 index 000000000..6016989d8 --- /dev/null +++ b/dev/tests/raycast.lua @@ -0,0 +1,47 @@ +local util = require "core:tests_util" + +-- Create world and prepare settings +util.create_demo_world("core:default") +app.set_setting("chunks.load-distance", 3) +app.set_setting("chunks.load-speed", 1) + +-- Create player +local pid = player.create("Xerxes") +player.set_spawnpoint(pid, 0, 100, 0) +player.set_pos(pid, 0, 100, 0) +app.sleep_until(function () return block.get(0, 0, 0) ~= -1 end) +app.sleep_until(function () return block.get(-5, 8, 2) ~= -1 end) + +local sand_idx = block.index("base:sand") +block.set(0, 1, 0, sand_idx) + +local start = {-5, 8, 2} +local target = {0.5, 1.5, 0.5} +local ray = world.raycast({ + start = start, + dir = vec3.normalize(vec3.sub(target, start)), + distance = 10, +}) +debug.print(ray) + +assert(ray, "ray didn't hit") +util.check_int(sand_idx, ray.block, "ray.block") +util.check_vec3({0, 1, 0}, ray.normal, "ray.normal") +util.check_vec3({0.0769, 2, 0.6153}, ray.endpoint, "ray.endpoint") +util.check_vec3({0, 1, 0}, ray.iendpoint, "ray.iendpoint") +util.check_float(7.9807, ray.length, "ray.length") + +ray = world.raycast({ + start = start, + dir = vec3.normalize(vec3.sub(target, start)), + distance = 10, + filter_blocks = {sand_idx}, +}) +debug.print(ray) + +assert(ray, "ray didn't hit") +util.check_int(block.index("core:obstacle"), ray.block, "ray.block") +util.check_vec3({0, 1, 0}, ray.normal, "ray.normal") +util.check_vec3({0.923, 1, 0.3846}, ray.endpoint, "ray.endpoint") +util.check_vec3({0, 0, 0}, ray.iendpoint, "ray.iendpoint") +util.check_float(9.3108, ray.length, "ray.length") 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/en/particles.md b/doc/en/particles.md index 66d3858d5..ecded4096 100644 --- a/doc/en/particles.md +++ b/doc/en/particles.md @@ -9,14 +9,14 @@ Particles are a table, all fields of which are optional. | lighting | Lighting. | true | | collision | Collision detection. | true | | max_distance | Maximum distance from the camera at which particles may spawn. | 16.0 | -| spawn_interval | Particle spawn interval in seconds. | 1.0 | +| spawn_interval | Particle spawn interval in seconds. Also initial spawn delay | 1.0 | | lifetime | Average lifetime of particles in seconds. | 5.0 | | lifetime_spread | Maximum deviation of particle lifetime (from 0.0 to 1.0). | 0.2 | | velocity | Initial linear velocity of particles. | {0, 0, 0} | | acceleration | Particles acceleration. | {0, -16, 0} | | explosion | Force of particles explosion on spawn. | {2, 2, 2} | | size | Size of particles. | {0.1, 0.1, 0.1} | -| size_spread | Maximum particle size spread over time. | 0.2 | +| size_spread | Maximum particle size spread. | 0.2 | | angle_spread | Maximum initial rotation angle spread (0 to 1) | 0.0 | | min_angular_vel | Minimum angular velocity (radians per sec). Non-negative. | 0.0 | | max_angular_vel | Maximum angular velocity (radians per sec). Non-negative. | 0.0 | @@ -35,7 +35,8 @@ gfx.particles.emit( count: int, -- particle settings table preset: table, - -- additional particle settings table + -- particle settings table overriding 'preset' parameters + -- use it instead of 'presets' table copying for modification [optional] extension: table ) -> int ``` @@ -63,7 +64,7 @@ Returns the static position or uid of the entity the emitter is bound to. Returns nil if the emitter does not exist. -``lua +```lua gfx.particles.set_origin(id: int, origin: vec3 | int) ``` 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/builtins/libblock.md b/doc/en/scripting/builtins/libblock.md index db407e92a..4513d57cf 100644 --- a/doc/en/scripting/builtins/libblock.md +++ b/doc/en/scripting/builtins/libblock.md @@ -159,6 +159,8 @@ The function returns a table with the results or nil if the ray does not hit any The result will use the destination table instead of creating a new one if the optional argument specified. +Use `world.raycast(...)` for more detailed configuration. + ## Model and physics ```lua diff --git a/doc/en/scripting/builtins/libentities.md b/doc/en/scripting/builtins/libentities.md index d65d95f05..9e653ba78 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 @@ -51,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 @@ -71,3 +74,5 @@ entities.raycast(start: vec3, dir: vec3, max_distance: number, ignore: int, [opt The function is an extended version of [block.raycast](libblock.md#raycast). Returns a table with the results if the ray touches a block or entity. Accordingly, this will affect the presence of the *entity* and *block* fields. + +Use `world.raycast(...)` for more detailed configuration. 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/builtins/libworld.md b/doc/en/scripting/builtins/libworld.md index b2453b34b..54b44fe15 100644 --- a/doc/en/scripting/builtins/libworld.md +++ b/doc/en/scripting/builtins/libworld.md @@ -70,4 +70,45 @@ world.save_chunk_data( -- compressed chunk data data: Bytearray ) + +-- Casts a ray from the start point in the dir direction. +world.raycast( + params: table { + [==[ required parameters ]==] + -- starting point + start: vec3, + -- ray direction vector + dir: vec3, + -- maximum distance + distance: number, + + [==[ additional parameters (entities) ]==] + -- consider entities + entities: bool = true, + -- uid of the ignored entity + ignore_uid: number = 0 (ENTITY_NONE), + -- id of the entity types used for filtering + filter_entities: table = nil, + -- exclusion filtering mode: + -- if true, filter_entities defines the list of ignored entity types + entities_exclusion: bool = false, + + [==[ additional parameters (blocks) ]==] + -- id of the block types used for filtering + filter_blocks: table = nil, + -- exclusion filtering mode: + -- if true, filter_blocks defines the list Ignored + blocks_exclusion: bool = true, + -- include blocks with selectable=false + nonselect_blocks: bool = false; + } +) -> { + block: int or nil, -- block id + entity: int or nil, -- entity uid + endpoint: vec3, -- ray touchpoint + iendpoint: vec3, -- position of the block touched by the ray + length: number, -- ray length + normal: vec3, -- normal vector of the surface touched by the ray +} or nil if the ray did not touch a block or entity +``` ``` diff --git a/doc/en/scripting/ecs.md b/doc/en/scripting/ecs.md index 878452928..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 @@ -264,3 +275,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/en/scripting/filesystem.md b/doc/en/scripting/filesystem.md index 8341e9665..12a190daa 100644 --- a/doc/en/scripting/filesystem.md +++ b/doc/en/scripting/filesystem.md @@ -52,6 +52,60 @@ yaml.parse(code: str) -> table Parses a YAML string into a table. +## XML Library + +The library contains functions for serializing and deserializing XML: + +Further, XML as a data type denotes a Lua table with the following structure: +- Element tag is accessible by the key `#` +- XML ​​attributes are accessible as key = value pairs +- Inner elements are accessible by index + +The table is used both as a dictionary and an array. + +Example: + +```xml + + + +``` + +Equivalent Lua table: + +```lua +{ + ['#'] = "panel", + size = "400", + color = "0", + interval = "1", + context = "menu", + { + ['#'] = "button", + onclick = 'menu.page="worlds"', + { + "@Worlds" + } + } +} +``` + +Consequently, the button element is accessible as `panel_element[1]`. + +```lua +-- Serializes a tree in XML format +-- multiline - use multiline formatting +xml.tostring(data: XML, multiline: bool = true) -> str + +-- Parses XML +xml.parse(code: str) -> XML + +-- Parses the VCD (VoxelCore Declaration) format used by the VCM format. +-- root_tag - a root element with the specified tag will be implicitly created, +-- containing all root elements +xml.parse_vcd(code: str, root_tag: str = "root") -> XML +``` + ## *bjson* library The library contains functions for working with the binary data exchange format [vcbjson](../../specs/binary_json_spec.md). diff --git a/doc/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/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* diff --git a/doc/ru/particles.md b/doc/ru/particles.md index 445ded593..e1e9fb34a 100644 --- a/doc/ru/particles.md +++ b/doc/ru/particles.md @@ -10,14 +10,14 @@ | lighting | Освещение. | true | | collision | Обнаружение столкновений. | true | | max_distance | Максимальная дистанция от камеры, при которой происходит спавн частиц. | 16.0 | -| spawn_interval | Интервал спавна частиц в секундах. | 1.0 | +| spawn_interval | Интервал спавна частиц в секундах, также задержка перед спавном первой. | 1.0 | | lifetime | Среднее время жизни частиц в секундах. | 5.0 | | lifetime_spread | Максимальное отклонение времени жизни частицы (от 0.0 до 1.0). | 0.2 | | velocity | Начальная линейная скорость частиц. | {0, 0, 0} | | acceleration | Ускорение частиц. | {0, -16, 0} | | explosion | Сила разлёта частиц при спавне. | {2, 2, 2} | | size | Размер частиц. | {0.1, 0.1, 0.1} | -| size_spread | Максимальное отклонение времени размера частиц. | 0.2 | +| size_spread | Максимальное отклонение размера частиц. | 0.2 | | angle_spread | Максимальное отклонение начального угла поворота (от 0 до 1) | 0.0 | | min_angular_vel | Минимальная угловая скорость (радианы в сек.). Неотрицательное. | 0.0 | | max_angular_vel | Максимальная угловая скорость (радианы в сек.). Неотрицательное. | 0.0 | @@ -36,7 +36,8 @@ gfx.particles.emit( count: int, -- таблица настроек частиц preset: table, - -- дополнительная таблица настроек частиц + -- таблица настроек частиц перезаписывающая значения параметров + -- используется вместо копирования и модификации 'preset' [опционально] extension: table ) -> int ``` 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/builtins/libblock.md b/doc/ru/scripting/builtins/libblock.md index b7af3e462..9725d2b3d 100644 --- a/doc/ru/scripting/builtins/libblock.md +++ b/doc/ru/scripting/builtins/libblock.md @@ -115,6 +115,8 @@ block.raycast(start: vec3, dir: vec3, max_distance: number, [опциональ Для результата будет использоваться целевая (dest) таблица вместо создания новой, если указан опциональный аргумент. +Для более тонкой настройки используйте `world.raycast(...)`. + ## Вращение ```lua @@ -209,4 +211,4 @@ block.get_field( name: string, [опционально] index: int = 0 ) -> any | nil -``` \ No newline at end of file +``` diff --git a/doc/ru/scripting/builtins/libentities.md b/doc/ru/scripting/builtins/libentities.md index f37dc1b32..9753c7e15 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 @@ -50,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 @@ -69,4 +72,6 @@ entities.raycast(start: vec3, dir: vec3, max_distance: number, ignore: int, [opt Функция является расширенным вариантом [block.raycast](libblock.md#raycast). Возвращает таблицу с результатами если луч касается блока, либо сущности. -Соответственно это повлияет на наличие полей *entity* и *block*. \ No newline at end of file +Соответственно это повлияет на наличие полей *entity* и *block*. + +Для более тонкой настройки используйте `world.raycast(...)`. diff --git a/doc/ru/scripting/builtins/libinput.md b/doc/ru/scripting/builtins/libinput.md index 7d04747a9..17df1f8aa 100644 --- a/doc/ru/scripting/builtins/libinput.md +++ b/doc/ru/scripting/builtins/libinput.md @@ -21,7 +21,7 @@ input.get_bindings() -> table input.get_binding_text(bindname: string) -> string -- Проверяет активность привязки. -input.is_active(bindname: string) -> string +input.is_active(bindname: string) -> boolean -- Включает/выключает привязку до выхода из мира. input.set_enabled(bindname: string, flag: boolean) @@ -74,4 +74,4 @@ input.add_callback("key:escape", function () print("NO") return true -- предотвращает вызов назначенных ранее функций end, document.root) -``` \ No newline at end of file +``` 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/builtins/libworld.md b/doc/ru/scripting/builtins/libworld.md index 215329df3..6b945aa76 100644 --- a/doc/ru/scripting/builtins/libworld.md +++ b/doc/ru/scripting/builtins/libworld.md @@ -69,4 +69,44 @@ world.save_chunk_data( -- сжатые данные чанка data: Bytearray ) -``` \ No newline at end of file + +-- Бросает луч из точки start в направлении dir. +world.raycast( + params: table { + [==[ обязательные параметры ]==] + -- стартовая точка + start: vec3, + -- вектор направления луча + dir: vec3, + -- максимальное расстояние + distance: number, + + [==[ дополнительные параметры (сущности) ]==] + -- учитывать сущности + entities: bool = true, + -- uid игнорируемой сущности + ignore_uid: number = 0 (ENTITY_NONE), + -- id типов сущностей используемых для фильтрации + filter_entities: table = nil, + -- исключающий режим фильтрации: + -- при значении (true) filter_entities определяет список игнорируемых + entities_exclusion: bool = false, + + [==[ дополнительные параметры (блоки) ]==] + -- id типов блоков используемых для фильтрации + filter_blocks: table = nil, + -- исключающий режим фильтрации: + -- при значении (true) filter_blocks определяет список игнорируемых + blocks_exclusion: bool = true, + -- учитывать блоки с selectable=false + nonselect_blocks: bool = false; + } +) -> { + block: int или nil, -- id блока + entity: int или nil, -- uid сущности + endpoint: vec3, -- точка касания луча + iendpoint: vec3, -- позиция блока, которого касается луч + length: number, -- длина луча + normal: vec3, -- вектор нормали поверхности, которой касается луч +} или nil если луч не коснулся блока или сущности +``` diff --git a/doc/ru/scripting/ecs.md b/doc/ru/scripting/ecs.md index 1a5f6ccf3..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] > При выходе сущности за пределы зоны прогрузки она удаляется, вызывая события. > При повторном попадании в зону прогрузки сущность спавнится заново. @@ -265,3 +276,8 @@ function on_used(playerid: int) Вызывается при использовании сущности (ПКМ по сущности). ID игрока передается в качестве аргумента. +```lua +function on_player_set(playerid: int) +``` + +Вызывается при прикреплении сущности к игроку. 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 +``` diff --git a/doc/ru/scripting/filesystem.md b/doc/ru/scripting/filesystem.md index 0e060d94c..a3bee78a6 100644 --- a/doc/ru/scripting/filesystem.md +++ b/doc/ru/scripting/filesystem.md @@ -52,6 +52,60 @@ yaml.parse(code: str) -> table Парсит YAML строку в таблицу. +## Библиотека xml + +Библиотека содержит функции для сериализации и десериализации XML: + +Далее XML как тип данных обозначает Lua-таблицу со следующей структурой: +- Тег элемента доступен по ключу `#` +- XML-аттрибуты доступны как пары ключ = значение +- Вложенные элементы доступны по индексу + +Таблица используется как словарь, так и массив. + +Пример: + +```xml + + + +``` + +Эквивалентная Lua-таблица: + +```lua +{ + ['#'] = "panel", + size = "400", + color = "0", + interval = "1", + context = "menu", + { + ['#'] = "button", + onclick = 'menu.page="worlds"', + { + "@Worlds" + } + } +} +``` + +Соответственно, элемент button доступен как `элемент_panel[1]`. + +```lua +-- Сериализует дерево в формате XML +-- multiline - использовать ли многострочное форматирование +xml.tostring(data: XML, multiline: bool = true) -> str + +-- Парсит XML +xml.parse(code: str) -> XML + +-- Парсит формат VCD (VoxelCore Declaration), используемый форматом VCM. +-- root_tag - будет неявно создан корневой элемент с указанным тегом, +-- содержащий все корневые элементы +xml.parse_vcd(code: str, root_tag: str | nil = nil) -> XML +``` + ## Библиотека bjson Библиотека содержит функции для работы с двоичным форматом обмена данными [vcbjson](../../specs/binary_json_spec.md). diff --git a/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 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/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 diff --git a/res/modules/tests_util.lua b/res/modules/tests_util.lua index 1e2207c24..e4ab73bc3 100644 --- a/res/modules/tests_util.lua +++ b/res/modules/tests_util.lua @@ -5,4 +5,25 @@ function util.create_demo_world(generator) app.new_world("demo", "2019", generator or "core:default") end +function util.check_vec3(expected, fact, name, delta) + delta = delta or 1e-4 + for i=1,3 do + assert(math.abs(fact[i] - expected[i]) < delta, + name.."["..i.."] expected: "..expected[i]..", fact: "..fact[i]) + end +end + +function util.check_float(expected, fact, name, delta) + delta = delta or 1e-4 + local actual_delta = math.abs(fact - expected) + assert(actual_delta < delta, + name.." expected: "..expected..", fact: "..fact.. + " (delta: "..actual_delta.." > "..delta..")") +end + +function util.check_int(expected, fact, name) + assert(fact == expected, + name.." expected: "..expected..", fact: "..fact) +end + return util diff --git a/res/scripts/components/mob.lua b/res/scripts/components/mob.lua index 9f824600f..9bddc4bcd 100644 --- a/res/scripts/components/mob.lua +++ b/res/scripts/components/mob.lua @@ -139,14 +139,16 @@ function get_dir() return mob_dir 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({mob_dir[3], mob_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/res/scripts/stdlib.lua b/res/scripts/stdlib.lua index 093501d75..b658fe096 100644 --- a/res/scripts/stdlib.lua +++ b/res/scripts/stdlib.lua @@ -312,6 +312,8 @@ entities.get_all = function(uids) return stdcomp.get_all(uids) end end +world.raycast = entities.__world_raycast +entities.__world_raycast = nil __vc_scripts_registry = require "core:internal/scripts_registry" diff --git a/src/coders/BasicParser.inl b/src/coders/BasicParser.inl index 796205b2a..5b249bfcf 100644 --- a/src/coders/BasicParser.inl +++ b/src/coders/BasicParser.inl @@ -22,9 +22,26 @@ namespace { } inline double power(double base, int64_t power) { + if (power == 0) { + return 1.0; + } + + uint64_t exp; + if (power < 0) { + base = 1.0 / base; + exp = static_cast(-(power + 1)) + 1; + } else { + exp = static_cast(power); + } + double result = 1.0; - for (int64_t i = 0; i < power; i++) { - result *= base; + + while (exp > 0) { + if (exp & 1) + result *= base; + + base *= base; + exp >>= 1; } return result; } 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/coders/xml.cpp b/src/coders/xml.cpp index 93e1c8031..b40ee7700 100644 --- a/src/coders/xml.cpp +++ b/src/coders/xml.cpp @@ -528,3 +528,11 @@ std::string xml::stringify( return ss.str(); } + +std::string xml::stringify( + const Node& element, bool nice, const std::string& indentStr +) { + std::stringstream ss; + stringifyElement(ss, element, nice, indentStr, 0); + return ss.str(); +} diff --git a/src/coders/xml.hpp b/src/coders/xml.hpp index 4954caa17..1a8a94ca5 100644 --- a/src/coders/xml.hpp +++ b/src/coders/xml.hpp @@ -119,6 +119,12 @@ namespace xml { const std::string& indentStr = " " ); + std::string stringify( + const Node& element, + bool nice = true, + const std::string& indentStr = " " + ); + /// @brief Read XML Document from string /// @param filename file name will be shown in error messages /// @param source xml source code string diff --git a/src/graphics/core/FontMetics.hpp b/src/graphics/core/FontMetics.hpp index a835a495b..d62a105d9 100644 --- a/src/graphics/core/FontMetics.hpp +++ b/src/graphics/core/FontMetics.hpp @@ -9,8 +9,8 @@ class Font; struct FontMetrics { std::optional> font; - int lineHeight; - int yoffset; + int lineHeight = 0; + int yoffset = 0; int _glyphInterval = 8; ~FontMetrics(); diff --git a/src/graphics/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/graphics/render/Decorator.cpp b/src/graphics/render/Decorator.cpp index b988898ce..1fa2abd0b 100644 --- a/src/graphics/render/Decorator.cpp +++ b/src/graphics/render/Decorator.cpp @@ -22,6 +22,7 @@ #include "voxels/Block.hpp" #include "voxels/Chunk.hpp" #include "voxels/Chunks.hpp" +#include "voxels/blocks_agent.hpp" #include "window/Camera.hpp" #include "world/Level.hpp" #include "WorldRenderer.hpp" @@ -98,7 +99,7 @@ void Decorator::updateAcoustics(const Camera& camera) { auto& chunks = *player.chunks; const auto& start = camera.position; - float rayLength = 40.0f; + float length = 40.0f; auto& blocks = level.content.getIndices()->blocks; @@ -107,7 +108,7 @@ void Decorator::updateAcoustics(const Camera& camera) { float averageDistance = 0.0f; float averageAbsorption = 0.0f; float speedOfSound = 300.0f; - float minDistance = rayLength; + float minDistance = length; for (int i = 0; i < rays; i++) { float u1 = random.randFloat(); float u2 = random.randFloat(); @@ -121,12 +122,13 @@ void Decorator::updateAcoustics(const Camera& camera) { glm::vec3 end; glm::ivec3 norm; glm::ivec3 iend; - auto vox = chunks.rayCast(start, dir, rayLength, end, norm, iend); + blocks_agent::RaycastSettings raycast {}; + auto vox = chunks.rayCast(start, dir, length, end, norm, iend, raycast); if (vox == nullptr) { continue; } auto distance = glm::distance(start, end); - if (distance >= rayLength * 0.98f) { + if (distance >= length * 0.98f) { continue; } if (distance < minDistance) { @@ -149,7 +151,7 @@ void Decorator::updateAcoustics(const Camera& camera) { averageAbsorption /= hit; } - float decayTime = 20.0f * (averageDistance / rayLength); + float decayTime = 20.0f * (averageDistance / length); float escapeRatio = static_cast(rays - hit) / static_cast(rays); acoustics.reverbGain = glm::pow(hit / static_cast(rays), 3.0f); diff --git a/src/graphics/ui/elements/Label.cpp b/src/graphics/ui/elements/Label.cpp index 14cbeea2d..effb79830 100644 --- a/src/graphics/ui/elements/Label.cpp +++ b/src/graphics/ui/elements/Label.cpp @@ -207,6 +207,9 @@ uint Label::getLinesNumber() const { void Label::draw(const DrawContext& pctx, const Assets& assets) { auto batch = pctx.getBatch2D(); auto font = assets.getShared(fontName); + if (font == nullptr) { + return; + } cache.prepare( font, font->getMetrics(), diff --git a/src/graphics/ui/elements/TextBox.cpp b/src/graphics/ui/elements/TextBox.cpp index d04ca5786..f37673fc1 100644 --- a/src/graphics/ui/elements/TextBox.cpp +++ b/src/graphics/ui/elements/TextBox.cpp @@ -353,7 +353,9 @@ void TextBox::draw(const DrawContext& pctx, const Assets& assets) { void TextBox::drawBackground(const DrawContext& pctx, const Assets& assets) { auto font = assets.getShared(label->getFontName()); - rawTextCache.prepare(font, font->getMetrics(), label->getSize().x); + if (font != nullptr) { + rawTextCache.prepare(font, font->getMetrics(), label->getSize().x); + } glm::vec2 pos = calcPos(); diff --git a/src/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/PlayerController.cpp b/src/logic/PlayerController.cpp index 909ca3778..03abc07b2 100644 --- a/src/logic/PlayerController.cpp +++ b/src/logic/PlayerController.cpp @@ -200,8 +200,13 @@ void CameraControl::update( auto castRay = [](Player& player, Camera& camera, const glm::vec3& front) -> glm::vec3 { auto blockEnd = player.chunks->rayCastToObstacle(camera.position, front, 3.0f); + + Entities::RaycastSettings raycast {}; + raycast.ignoredUid = player.getEntity(); + raycast.solidEntitiesOnly = true; + auto entityRay = player.getLevel().entities->rayCast( - camera.position, front, 3.0f, player.getEntity(), true + camera.position, front, 3.0f, raycast ); if (entityRay.has_value() && entityRay->distance < glm::distance(camera.position, blockEnd)) { @@ -350,7 +355,13 @@ voxel* PlayerController::updateSelection(float maxDistance) { glm::ivec3 iend; glm::ivec3 norm; voxel* vox = chunks.rayCast( - camera->position, camera->front, maxDistance, end, norm, iend + camera->position, + camera->front, + maxDistance, + end, + norm, + iend, + blocks_agent::RaycastSettings {} ); if (vox) { maxDistance = glm::distance(camera->position, end); @@ -358,8 +369,12 @@ voxel* PlayerController::updateSelection(float maxDistance) { auto prevEntity = selection.entity; selection.entity = ENTITY_NONE; selection.actualPosition = iend; + + Entities::RaycastSettings raycast {}; + raycast.ignoredUid = player.getEntity(); + if (auto result = level.entities->rayCast( - camera->position, camera->front, maxDistance, player.getEntity() + camera->position, camera->front, maxDistance, raycast )) { selection.entity = result->entity; selection.hitPosition = 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/libblock.cpp b/src/logic/scripting/lua/libs/libblock.cpp index 6c9550dde..fc26ced38 100644 --- a/src/logic/scripting/lua/libs/libblock.cpp +++ b/src/logic/scripting/lua/libs/libblock.cpp @@ -543,13 +543,16 @@ static int l_destruct(lua::State* L) { } static int l_raycast(lua::State* L) { - auto& level = require_level(); + const auto& level = require_level(); auto start = lua::tovec<3>(L, 1); auto dir = lua::tovec<3>(L, 2); auto maxDistance = lua::tonumber(L, 3); - bool includeNonSelectable = false; + + blocks_agent::RaycastSettings raycast {}; std::set filteredBlocks {}; + raycast.filter = &filteredBlocks; + const int luaStackSize = lua::gettop(L); if (luaStackSize >= 5) { if (lua::istable(L, 5)) { @@ -568,21 +571,14 @@ static int l_raycast(lua::State* L) { } } if (luaStackSize >= 6) { - includeNonSelectable = lua::toboolean(L, 6); + raycast.includeNonSelectable = lua::toboolean(L, 6); } glm::vec3 end; glm::ivec3 normal; glm::ivec3 iend; + if (auto voxel = blocks_agent::raycast( - *level.chunks, - start, - dir, - maxDistance, - end, - normal, - iend, - filteredBlocks, - includeNonSelectable + *level.chunks, start, dir, maxDistance, end, normal, iend, raycast )) { if (luaStackSize >= 4 && !lua::isnil(L, 4)) { lua::pushvalue(L, 4); diff --git a/src/logic/scripting/lua/libs/libentity.cpp b/src/logic/scripting/lua/libs/libentity.cpp index d2c0c4bc5..110aee123 100644 --- a/src/logic/scripting/lua/libs/libentity.cpp +++ b/src/logic/scripting/lua/libs/libentity.cpp @@ -17,6 +17,8 @@ #include "voxels/blocks_agent.hpp" #include "window/Camera.hpp" +#include + using namespace scripting; static const EntityDef* require_entity_def(lua::State* L) { @@ -48,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()); } @@ -145,34 +154,59 @@ static int l_get_all_in_radius(lua::State* L) { return 1; } -static int l_raycast(lua::State* L) { - auto start = lua::tovec<3>(L, 1); - auto dir = lua::tovec<3>(L, 2); - auto maxDistance = lua::tonumber(L, 3); - auto ignoreEntityId = lua::tointeger(L, 4); - bool includeNonSelectable = false; - std::set filteredBlocks {}; - const int luaStackSize = lua::gettop(L); - if (luaStackSize >= 6) { - if (lua::istable(L, 6)) { - int addLen = lua::objlen(L, 6); - for (int i = 0; i < addLen; i++) { - lua::rawgeti(L, i + 1, 6); - auto blockName = std::string(lua::tostring(L, -1)); - const Block* block = content->blocks.find(blockName); - if (block != nullptr) { - filteredBlocks.insert(block->rt.id); - } - lua::pop(L); - } - } else { - throw std::runtime_error("table expected for filter"); - } +static int l_push_ray( + lua::State* L, + const std::optional& ray, + blockid_t block, + const glm::vec3& start, + const glm::vec3& dir, + const glm::vec3& end, + const glm::ivec3& iend, + const glm::ivec3& normal +) { + if (!ray.has_value() && block == BLOCK_VOID) { + return 0; } - if (luaStackSize >= 7) { - includeNonSelectable = lua::toboolean(L, 6); + if (lua::gettop(L) >= 5 && lua::istable(L, 5)) { + lua::pushvalue(L, 5); + } else { + lua::createtable(L, 0, 6); } + lua::pushvec3(L, ray ? (start + dir * ray->distance) : end); + lua::setfield(L, "endpoint"); + + lua::pushvec3(L, ray ? ray->normal : normal); + lua::setfield(L, "normal"); + + lua::pushnumber(L, glm::distance(start, end)); + lua::setfield(L, "length"); + + lua::pushvec3(L, iend); + lua::setfield(L, "iendpoint"); + + lua::pushinteger(L, block); + lua::setfield(L, "block"); + + if (ray.has_value()) { + lua::pushinteger(L, ray->entity); + lua::setfield(L, "entity"); + } else if (block != BLOCK_VOID) { + lua::pushinteger(L, block); + lua::setfield(L, "block"); + } + return 1; +} + +static int l_raycast( + lua::State* L, + const glm::vec3& start, + const glm::vec3& dir, + float maxDistance, + const blocks_agent::RaycastSettings& voxelRaycast, + const Entities::RaycastSettings& entityRaycast, + bool entities +) { glm::vec3 end; glm::ivec3 normal; glm::ivec3 iend; @@ -187,61 +221,139 @@ static int l_raycast(lua::State* L) { end, normal, iend, - filteredBlocks, - includeNonSelectable + voxelRaycast )) { maxDistance = glm::distance(start, end); block = voxel->id; } - if (auto ray = - level->entities->rayCast(start, dir, maxDistance, ignoreEntityId)) { - if (luaStackSize >= 5 && !lua::isnil(L, 5)) { - lua::pushvalue(L, 5); + auto ray = + entities + ? level->entities->rayCast(start, dir, maxDistance, entityRaycast) + : std::nullopt; + return l_push_ray(L, ray, block, start, dir, end, iend, normal); +} + +static void load_blocks_filter(lua::State* L, int idx, std::set& dst) { + int count = lua::objlen(L, idx); + for (int i = 0; i < count; i++) { + lua::rawgeti(L, i + 1, idx); + if (lua::isnumber(L, -1)) { + dst.insert(lua::touinteger(L, -1)); } else { - lua::createtable(L, 0, 6); + auto blockName = std::string(lua::tostring(L, -1)); + const Block* block = content->blocks.find(blockName); + if (block != nullptr) { + dst.insert(block->rt.id); + } } - - lua::pushvec3(L, start + dir * ray->distance); - lua::setfield(L, "endpoint"); - - lua::pushvec3(L, ray->normal); - lua::setfield(L, "normal"); - - lua::pushnumber(L, glm::distance(start, end)); - lua::setfield(L, "length"); - - lua::pushvec3(L, iend); - lua::setfield(L, "iendpoint"); - - lua::pushinteger(L, block); - lua::setfield(L, "block"); - - lua::pushinteger(L, ray->entity); - lua::setfield(L, "entity"); - return 1; - } else if (block != BLOCK_VOID) { - if (lua::gettop(L) >= 5 && !lua::isnil(L, 5)) { - lua::pushvalue(L, 5); - } else { - lua::createtable(L, 0, 5); - } - lua::pushvec3(L, end); - lua::setfield(L, "endpoint"); - - lua::pushvec3(L, normal); - lua::setfield(L, "normal"); - - lua::pushnumber(L, glm::distance(start, end)); - lua::setfield(L, "length"); - - lua::pushvec3(L, iend); - lua::setfield(L, "iendpoint"); - - lua::pushinteger(L, block); - lua::setfield(L, "block"); - return 1; + lua::pop(L); } - return 0; +} + +static void load_entities_filter(lua::State* L, int idx, std::set& dst) { + int count = lua::objlen(L, idx); + for (int i = 0; i < count; i++) { + lua::rawgeti(L, i + 1, idx); + if (lua::isnumber(L, -1)) { + dst.insert(lua::touinteger(L, -1)); + } else { + auto entityName = std::string(lua::tostring(L, -1)); + const EntityDef* entity = content->entities.find(entityName); + if (entity != nullptr) { + dst.insert(entity->rt.id); + } + } + lua::pop(L); + } +} + +static int l_world_raycast(lua::State* L) { + blocks_agent::RaycastSettings blocksRaycast {}; + Entities::RaycastSettings entityRaycast {}; + + std::set filteredBlocks {}; + std::set filteredEntities {}; + + glm::vec3 start, dir; + float maxDistance; + bool entities = true; + + if (!lua::istable(L, 1)) { + throw std::runtime_error("invalid function overload"); + } + + lua::requirefield(L, "start", 1); + start = lua::tovec3(L, -1); + + lua::requirefield(L, "dir", 1); + dir = lua::tovec3(L, -1); + + lua::requirefield(L, "distance", 1); + maxDistance = lua::tonumber(L, -1); + + lua::pop(L, 3); + + if (lua::getfield(L, "entities", 1)) { + entities = lua::toboolean(L, -1); + lua::pop(L); + } + + if (lua::getfield(L, "ignore_uid", 1)) { + entityRaycast.ignoredUid = lua::tointeger(L, -1); + lua::pop(L); + } + if (lua::getfield(L, "filter_entities")) { + load_entities_filter(L, -1, filteredEntities); + blocksRaycast.filter = &filteredEntities; + lua::pop(L); + } + if (lua::getfield(L, "entities_exclusion")) { + entityRaycast.entityFilterExcludeMode = lua::toboolean(L, -1); + lua::pop(L); + } + if (lua::getfield(L, "filter_blocks", 1)) { + load_blocks_filter(L, -1, filteredBlocks); + blocksRaycast.filter = &filteredBlocks; + lua::pop(L); + } + if (lua::getfield(L, "blocks_exclusion")) { + blocksRaycast.blocksFilterExcludeMode = lua::toboolean(L, -1); + lua::pop(L); + } + if (lua::getfield(L, "nonselect_blocks", 1)) { + blocksRaycast.includeNonSelectable = lua::toboolean(L, -1); + lua::pop(L); + } + return l_raycast( + L, start, dir, maxDistance, blocksRaycast, entityRaycast, entities + ); +} + +static int l_raycast(lua::State* L) { + blocks_agent::RaycastSettings blocksRaycast {}; + Entities::RaycastSettings entityRaycast {}; + std::set filteredBlocks {}; + + const int luaStackSize = lua::gettop(L); + glm::vec3 start = lua::tovec<3>(L, 1); + glm::vec3 dir = lua::tovec<3>(L, 2); + float maxDistance = lua::tonumber(L, 3); + + entityRaycast.ignoredUid = lua::tointeger(L, 4); + if (luaStackSize >= 6) { + if (lua::istable(L, 6)) { + load_blocks_filter(L, 6, filteredBlocks); + blocksRaycast.filter = &filteredBlocks; + } else { + throw std::runtime_error("table expected for filter"); + } + } + if (luaStackSize >= 7) { + blocksRaycast.includeNonSelectable = lua::toboolean(L, 6); + } + return l_raycast( + L, start, dir, maxDistance, blocksRaycast, entityRaycast, true + ); } static int l_reload_component(lua::State* L) { @@ -268,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}, @@ -278,6 +391,7 @@ const luaL_Reg entitylib[] = { {"get_all_in_box", lua::wrap}, {"get_all_in_radius", lua::wrap}, {"raycast", lua::wrap}, + {"__world_raycast", lua::wrap}, {"reload_component", lua::wrap}, {nullptr, nullptr} }; 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/libs/libxml.cpp b/src/logic/scripting/lua/libs/libxml.cpp new file mode 100644 index 000000000..70ccc36bb --- /dev/null +++ b/src/logic/scripting/lua/libs/libxml.cpp @@ -0,0 +1,105 @@ +#include "coders/xml.hpp" +#include "api_lua.hpp" + +static const char* TAG_ATTR = "#"; +static const char* DEFAULT_ROOT_TAG = "root"; + +static int push_xml(lua::State* L, const xml::xmlelement& elem) { + if (elem.isText()) { + return lua::pushlstring(L, elem.getInnerText()); + } + + const auto& attrs = elem.getAttributes(); + const auto& elems = elem.getElements(); + const auto& tag = elem.getTag(); + + lua::createtable(L, elem.size(), attrs.size() + 1); + lua::pushlstring(L, tag); + lua::setfield(L, TAG_ATTR); + + for (int i = 0; i < elems.size(); i++) { + push_xml(L, *elems[i]); + lua::rawseti(L, i + 1); + } + + for (const auto& [key, value] : attrs) { + lua::pushlstring(L, value.getText()); + lua::setfield(L, key); + } + return 1; +} + +static std::unique_ptr toxml(lua::State* L) { + lua::getfield(L, TAG_ATTR); + auto tag = lua::require_lstring(L, -1); + lua::pop(L); + + auto elem = std::make_unique(std::string(tag)); + int length = lua::objlen(L, -1); + + for (int i = 0; i < length; i++) { + lua::rawgeti(L, i + 1); + elem->add(toxml(L)); + lua::pop(L); + } + + lua::pushnil(L); + while (lua::next(L, -2)) { + lua::pushvalue(L, -2); + do { + if (!lua::isstring(L, -1)) { + break; + } + auto key = lua::require_lstring(L, -1); + if (key == "@") { + break; + } + auto value = lua::require_lstring(L, -2); + elem->set(std::string(key), std::string(value)); + } while (false); + + lua::pop(L, 2); + } + return elem; +} + +static int l_tostring(lua::State* L) { + bool nice = lua::isboolean(L, 2) ? lua::toboolean(L, 2) : true; + lua::pushvalue(L, 1); + auto xml = toxml(L); + lua::pop(L); + return lua::pushstring(L, xml::stringify(*xml, nice)); +} + +static int l_parse(lua::State* L) { + auto string = lua::require_string(L, 1); + auto document = xml::parse("[string]", string); + return push_xml(L, *document->getRoot()); +} + + +static int l_parse_vcd(lua::State* L) { + auto string = lua::require_string(L, 1); + auto rootTag = lua::tostring(L, 2); + auto document = xml::parse_vcm( + "[string]", string, rootTag ? rootTag : "" + ); + const auto& root = *document->getRoot(); + if (rootTag != nullptr) { + return push_xml(L, root); + } + int rootElements = root.size(); + if (rootElements != 1) { + throw std::runtime_error( + "one root element expected, got " + std::to_string(rootElements) + ); + } + return push_xml(L, *root.getElements()[0]); +} + +const luaL_Reg xmllib[] = { + {"tostring", lua::wrap}, + {"parse", lua::wrap}, + {"parse_vcd", lua::wrap}, + {nullptr, nullptr} +}; diff --git a/src/logic/scripting/lua/lua_engine.cpp b/src/logic/scripting/lua/lua_engine.cpp index afb2f42eb..276dbaa74 100644 --- a/src/logic/scripting/lua/lua_engine.cpp +++ b/src/logic/scripting/lua/lua_engine.cpp @@ -66,6 +66,7 @@ static void create_libs(State* L, StateType stateType) { openlib(L, "vec2", vec2lib); openlib(L, "vec3", vec3lib); openlib(L, "vec4", vec4lib); + openlib(L, "xml", xmllib); openlib(L, "yaml", yamllib); openlib(L, "__vc_app", applib); 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/Entities.cpp b/src/objects/Entities.cpp index 61b312a24..fafd99fe5 100644 --- a/src/objects/Entities.cpp +++ b/src/objects/Entities.cpp @@ -166,8 +166,7 @@ std::optional Entities::rayCast( glm::vec3 start, glm::vec3 dir, float maxDistance, - entityid_t ignore, - bool solidOnly + const RaycastSettings& settings ) { Ray ray(start, dir); auto view = registry->view(); @@ -176,10 +175,18 @@ std::optional Entities::rayCast( glm::ivec3 foundNormal; for (auto [entity, eid, transform, body] : view.each()) { - if (eid.uid == ignore || !body.enabled || (solidOnly && !eid.def.solid)) { + if (eid.uid == settings.ignoredUid || !body.enabled || + (settings.solidEntitiesOnly && !eid.def.solid)) { continue; } - auto& hitbox = body.hitbox; + if (settings.entitiesFilter) { + bool matches = settings.entitiesFilter->find(eid.def.rt.id) != + settings.entitiesFilter->end(); + if (matches == settings.entityFilterExcludeMode) { + continue; + } + } + const auto& hitbox = body.hitbox; glm::ivec3 normal; double distance; if (ray.intersectAABB( diff --git a/src/objects/Entities.hpp b/src/objects/Entities.hpp index 33786118a..0d003ffb1 100644 --- a/src/objects/Entities.hpp +++ b/src/objects/Entities.hpp @@ -5,6 +5,7 @@ #include #include +#include "constants.hpp" #include "physics/Hitbox.hpp" #include "Transform.hpp" #include "Rigidbody.hpp" @@ -46,12 +47,6 @@ class Entities final { ); void preparePhysics(float delta); public: - struct RaycastResult { - entityid_t entity; - glm::ivec3 normal; - float distance; - }; - Entities(Level& level); ~Entities(); @@ -82,20 +77,33 @@ public: std::optional get(entityid_t id); + struct RaycastSettings { + /// @param ignore Ignored entity uid + entityid_t ignoredUid; + /// @param solidEntitiesOnly If true, only entities with solid hitboxes + /// will be checked + bool solidEntitiesOnly = false; + bool entityFilterExcludeMode = false; + const std::set* entitiesFilter = nullptr; + }; + + struct RaycastResult { + entityid_t entity; + glm::ivec3 normal; + float distance; + }; + /// @brief Entities raycast. No blocks check included, use combined with /// Chunks.rayCast /// @param start Ray start /// @param dir Ray direction normalized vector /// @param maxDistance Max ray length - /// @param ignore Ignored entity ID - /// @param solidOnly If true, only entities with solid hitboxes will be checked /// @return An optional structure containing entity, normal and distance std::optional rayCast( glm::vec3 start, glm::vec3 dir, float maxDistance, - entityid_t ignore = -1, - bool solidOnly = false + const RaycastSettings& settings ); void loadEntities(dv::value map); 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/util/SmallHeap.hpp b/src/util/SmallHeap.hpp index 8a6ff9290..6ff8cf238 100644 --- a/src/util/SmallHeap.hpp +++ b/src/util/SmallHeap.hpp @@ -11,8 +11,10 @@ namespace util { template - inline T read_int_le(const uint8_t* src, size_t offset=0) { - return dataio::le2h(*(reinterpret_cast(src) + offset)); + inline T read_int_le(const uint8_t* src) { + T tmp; + std::memcpy(&tmp, src, sizeof(T)); + return dataio::le2h(tmp); } /// @brief Simple heap implementation for memory-optimal sparse array of @@ -87,8 +89,7 @@ namespace util { std::memset(found, 0, entrySize); return found; } - this->free(found); - return allocate(index, size); + free(found); } for (size_t i = 0; i < entriesCount; i++) { auto data = buffer.data() + offset; @@ -110,9 +111,12 @@ namespace util { entriesCount++; auto data = buffer.data() + offset; - *reinterpret_cast(data) = dataio::h2le(index); + + Tindex indexTmp = dataio::h2le(index); + Tsize sizeTmp = dataio::h2le(size); + std::memcpy(data, &indexTmp, sizeof(Tindex)); data += sizeof(Tindex); - *reinterpret_cast(data) = dataio::h2le(size); + std::memcpy(data, &sizeTmp, sizeof(Tsize)); return data + sizeof(Tsize); } @@ -122,7 +126,7 @@ namespace util { if (ptr == nullptr) { return 0; } - return read_int_le(ptr, -1); + return read_int_le(ptr - sizeof(Tsize)); } /// @return number of entries @@ -147,7 +151,8 @@ namespace util { ubyte* dst = out.data(); const ubyte* src = buffer.data(); - *reinterpret_cast(dst) = dataio::h2le(entriesCount); + Tindex countTmp = dataio::h2le(entriesCount); + std::memcpy(dst, &countTmp, sizeof(Tindex)); dst += sizeof(Tindex); std::memcpy(dst, src, buffer.size()); @@ -172,7 +177,7 @@ namespace util { ) : buffer(buffer), index(index), offset(offset) {} Tsize size() const { - return read_int_le(buffer.data() + offset, -1); + return read_int_le(buffer.data() + offset - sizeof(Tsize)); } bool operator!=(const const_iterator& o) const { diff --git a/src/voxels/Chunks.cpp b/src/voxels/Chunks.cpp index 2a85b38bd..615ed2fbb 100644 --- a/src/voxels/Chunks.cpp +++ b/src/voxels/Chunks.cpp @@ -215,11 +215,10 @@ voxel* Chunks::rayCast( glm::vec3& end, glm::ivec3& norm, glm::ivec3& iend, - std::set filter, - bool includeNonSelectable + const blocks_agent::RaycastSettings& settings ) const { return blocks_agent::raycast( - *this, start, dir, maxDist, end, norm, iend, std::move(filter), includeNonSelectable + *this, start, dir, maxDist, end, norm, iend, settings ); } diff --git a/src/voxels/Chunks.hpp b/src/voxels/Chunks.hpp index 9fdebff1c..8b94026cb 100644 --- a/src/voxels/Chunks.hpp +++ b/src/voxels/Chunks.hpp @@ -24,6 +24,10 @@ class VoxelsVolume; template class StaticVoxelsVolume; +namespace blocks_agent { + struct RaycastSettings; +} + /// Player-centred chunks matrix class Chunks { LevelEvents* events; @@ -112,8 +116,7 @@ public: glm::vec3& end, glm::ivec3& norm, glm::ivec3& iend, - std::set filter = {}, - bool includeNonSelectable = false + const blocks_agent::RaycastSettings& settings ) const; glm::vec3 rayCastToObstacle( diff --git a/src/voxels/blocks_agent.cpp b/src/voxels/blocks_agent.cpp index 436372457..d72ddcceb 100644 --- a/src/voxels/blocks_agent.cpp +++ b/src/voxels/blocks_agent.cpp @@ -223,8 +223,7 @@ static inline voxel* raycast_blocks( glm::vec3& end, glm::ivec3& norm, glm::ivec3& iend, - std::set filter, - bool includeNonSelectable + const RaycastSettings& settings ) { const auto& blocks = chunks.getContentIndices().blocks; float px = start.x; @@ -240,9 +239,8 @@ static inline voxel* raycast_blocks( int iy = std::floor(py); int iz = std::floor(pz); - int stepx = (dx > 0.0f) ? 1 : -1; - int stepy = (dy > 0.0f) ? 1 : -1; - int stepz = (dz > 0.0f) ? 1 : -1; + glm::ivec3 step { + (dx > 0.0f) ? 1 : -1, (dy > 0.0f) ? 1 : -1, (dz > 0.0f) ? 1 : -1}; constexpr float infinity = std::numeric_limits::infinity(); constexpr float epsilon = 1e-6f; // 0.000001 @@ -250,31 +248,31 @@ static inline voxel* raycast_blocks( float tyDelta = (std::fabs(dy) < epsilon) ? infinity : std::fabs(1.0f / dy); float tzDelta = (std::fabs(dz) < epsilon) ? infinity : std::fabs(1.0f / dz); - float xdist = (stepx > 0) ? (ix + 1 - px) : (px - ix); - float ydist = (stepy > 0) ? (iy + 1 - py) : (py - iy); - float zdist = (stepz > 0) ? (iz + 1 - pz) : (pz - iz); + float xdist = (step.x > 0) ? (ix + 1 - px) : (px - ix); + float ydist = (step.y > 0) ? (iy + 1 - py) : (py - iy); + float zdist = (step.z > 0) ? (iz + 1 - pz) : (pz - iz); float txMax = (txDelta < infinity) ? txDelta * xdist : infinity; float tyMax = (tyDelta < infinity) ? tyDelta * ydist : infinity; float tzMax = (tzDelta < infinity) ? tzDelta * zdist : infinity; int steppedIndex = -1; - + bool includeNonSelectable = settings.includeNonSelectable; + while (t <= maxDist) { voxel* voxel = get(chunks, ix, iy, iz); if (voxel == nullptr) { return nullptr; } + auto filter = settings.filter; const auto& def = blocks.require(voxel->id); - if (voxel->id != BLOCK_AIR && (def.selectable || includeNonSelectable) && - (filter.empty() || filter.find(def.rt.id) == filter.end())) { - end.x = px + t * dx; - end.y = py + t * dy; - end.z = pz + t * dz; - iend.x = ix; - iend.y = iy; - iend.z = iz; + if (voxel->id != BLOCK_AIR && + (def.selectable || includeNonSelectable) && + ((filter == nullptr || filter->find(def.rt.id) == filter->end()) == + settings.blocksFilterExcludeMode)) { + end = start + t * dir; + iend = {ix, iy, iz}; if (!def.rt.solid) { const std::vector& hitboxes = @@ -309,51 +307,43 @@ static inline voxel* raycast_blocks( if (hit) return voxel; } else { - iend.x = ix; - iend.y = iy; - iend.z = iz; - - norm.x = norm.y = norm.z = 0; - if (steppedIndex == 0) norm.x = -stepx; - if (steppedIndex == 1) norm.y = -stepy; - if (steppedIndex == 2) norm.z = -stepz; + iend = {ix, iy, iz}; + norm = {0, 0, 0}; + if (steppedIndex != -1) { + norm[steppedIndex] = -step[steppedIndex]; + } return voxel; } } if (txMax < tyMax) { if (txMax < tzMax) { - ix += stepx; + ix += step.x; t = txMax; txMax += txDelta; steppedIndex = 0; } else { - iz += stepz; + iz += step.z; t = tzMax; tzMax += tzDelta; steppedIndex = 2; } } else { if (tyMax < tzMax) { - iy += stepy; + iy += step.y; t = tyMax; tyMax += tyDelta; steppedIndex = 1; } else { - iz += stepz; + iz += step.z; t = tzMax; tzMax += tzDelta; steppedIndex = 2; } } } - iend.x = ix; - iend.y = iy; - iend.z = iz; - - end.x = px + t * dx; - end.y = py + t * dy; - end.z = pz + t * dz; - norm.x = norm.y = norm.z = 0; + iend = {ix, iy, iz}; + end = start + t * dir; + norm = {0, 0, 0}; return nullptr; } @@ -365,10 +355,9 @@ voxel* blocks_agent::raycast( glm::vec3& end, glm::ivec3& norm, glm::ivec3& iend, - std::set filter, - bool includeNonSelectable + const RaycastSettings& settings ) { - return raycast_blocks(chunks, start, dir, maxDist, end, norm, iend, filter, includeNonSelectable); + return raycast_blocks(chunks, start, dir, maxDist, end, norm, iend, settings); } voxel* blocks_agent::raycast( @@ -379,10 +368,9 @@ voxel* blocks_agent::raycast( glm::vec3& end, glm::ivec3& norm, glm::ivec3& iend, - std::set filter, - bool includeNonSelectable + const RaycastSettings& settings ) { - return raycast_blocks(chunks, start, dir, maxDist, end, norm, iend, filter, includeNonSelectable); + return raycast_blocks(chunks, start, dir, maxDist, end, norm, iend, settings); } // reduce nesting on next modification diff --git a/src/voxels/blocks_agent.hpp b/src/voxels/blocks_agent.hpp index a86b2e25b..22144c618 100644 --- a/src/voxels/blocks_agent.hpp +++ b/src/voxels/blocks_agent.hpp @@ -412,6 +412,12 @@ inline void set_rotation( } } +struct RaycastSettings { + const std::set* filter = nullptr; + bool blocksFilterExcludeMode = true; + bool includeNonSelectable = false; +}; + /// @brief Cast ray to a selectable block with filter based on id. /// @param chunks chunks matrix /// @param start ray start position @@ -431,8 +437,7 @@ voxel* raycast( glm::vec3& end, glm::ivec3& norm, glm::ivec3& iend, - std::set filter, - bool includeNonSelectable + const RaycastSettings& settings ); /// @brief Cast ray to a selectable block with filter based on id. @@ -454,8 +459,7 @@ voxel* raycast( glm::vec3& end, glm::ivec3& norm, glm::ivec3& iend, - std::set filter, - bool includeNonSelectable + const RaycastSettings& settings ); void get_voxels( diff --git a/src/window/detail/GLFWWindow.cpp b/src/window/detail/GLFWWindow.cpp index db3d67241..394472783 100644 --- a/src/window/detail/GLFWWindow.cpp +++ b/src/window/detail/GLFWWindow.cpp @@ -33,6 +33,7 @@ static void init_gl_extensions_list() { } } +[[maybe_unused]] static bool is_gl_extension_supported(const char *extension) { if (!extension || !*extension) { return false; @@ -40,6 +41,7 @@ static bool is_gl_extension_supported(const char *extension) { return supported_gl_extensions.find(extension) != supported_gl_extensions.end(); } +#ifndef __APPLE__ static const char* gl_error_name(int error) { switch (error) { case GL_DEBUG_TYPE_ERROR: return "ERROR"; @@ -80,6 +82,7 @@ static void GLAPIENTRY gl_message_callback( logger.warning() << "GL:" << gl_error_name(type) << ":" << gl_severity_name(severity) << ": " << message; } +#endif static bool initialize_gl(int width, int height) { glewExperimental = GL_TRUE; diff --git a/test/coders/json.cpp b/test/coders/json.cpp index be9f0534d..bf3d53a91 100644 --- a/test/coders/json.cpp +++ b/test/coders/json.cpp @@ -8,6 +8,8 @@ TEST(JSON, EncodeDecode) { const int bytesSize = 20; const int year = 2019; const float score = 3.141592; + const double distance = 1.23e45; + const double epsilon = 1e-6; const bool visible = true; dv::objects::Bytes srcBytes(bytesSize); for (int i = 0; i < bytesSize; i ++) { @@ -20,6 +22,8 @@ TEST(JSON, EncodeDecode) { object["name"] = name; object["year"] = year; object["score"] = score; + object["distance"] = distance; + object["epsilon"] = epsilon; object["visible"] = visible; object["data"] = srcBytes; @@ -30,6 +34,8 @@ TEST(JSON, EncodeDecode) { EXPECT_EQ(object["name"].asString(), name); EXPECT_EQ(object["year"].asInteger(), year); EXPECT_FLOAT_EQ(object["score"].asNumber(), score); + EXPECT_DOUBLE_EQ(object["distance"].asNumber(), distance); + EXPECT_DOUBLE_EQ(object["epsilon"].asNumber(), epsilon); EXPECT_EQ(object["visible"].asBoolean(), visible); auto b64string = object["data"].asString(); diff --git a/test/util/SmallHeap.cpp b/test/util/SmallHeap.cpp index c5b3adf28..a4ecab842 100644 --- a/test/util/SmallHeap.cpp +++ b/test/util/SmallHeap.cpp @@ -8,9 +8,13 @@ TEST(SmallHeap, Allocation) { auto index = 0; auto size = 4; + std::cout << "step 1" << std::endl; SmallHeap map; + std::cout << "step 2" << std::endl; auto ptr = map.allocate(index, size); + std::cout << "ptr: " << (void*)ptr << " le2h(1): " << dataio::le2h(1) << " h2le(1): " << dataio::h2le(1) << std::endl; EXPECT_EQ(map.sizeOf(ptr), size); + std::cout << "step 4" << std::endl; EXPECT_EQ(ptr, map.find(index)); }