Merge branch 'main' into animation

This commit is contained in:
MihailRis 2026-07-22 23:56:17 +03:00
commit 7c06a74244
67 changed files with 1231 additions and 260 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -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)

47
dev/tests/raycast.lua Normal file
View file

@ -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")

View file

@ -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`. 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 - `none` - display disabled
- `number` - number - `number` - number
- `relation` - current value to initial value (x/y) - `relation` - current value to initial value (x/y)
- `vbar` - vertical scale (used by default) - `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
Tags allow you to designate general properties of items. Names should be formatted as `prefix:tag_name`. Tags allow you to designate general properties of items. Names should be formatted as `prefix:tag_name`.

View file

@ -9,14 +9,14 @@ Particles are a table, all fields of which are optional.
| lighting | Lighting. | true | | lighting | Lighting. | true |
| collision | Collision detection. | true | | collision | Collision detection. | true |
| max_distance | Maximum distance from the camera at which particles may spawn. | 16.0 | | 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 | Average lifetime of particles in seconds. | 5.0 |
| lifetime_spread | Maximum deviation of particle lifetime (from 0.0 to 1.0). | 0.2 | | lifetime_spread | Maximum deviation of particle lifetime (from 0.0 to 1.0). | 0.2 |
| velocity | Initial linear velocity of particles. | {0, 0, 0} | | velocity | Initial linear velocity of particles. | {0, 0, 0} |
| acceleration | Particles acceleration. | {0, -16, 0} | | acceleration | Particles acceleration. | {0, -16, 0} |
| explosion | Force of particles explosion on spawn. | {2, 2, 2} | | explosion | Force of particles explosion on spawn. | {2, 2, 2} |
| size | Size of particles. | {0.1, 0.1, 0.1} | | 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 | | 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 | | 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 | | max_angular_vel | Maximum angular velocity (radians per sec). Non-negative. | 0.0 |
@ -35,7 +35,8 @@ gfx.particles.emit(
count: int, count: int,
-- particle settings table -- particle settings table
preset: 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 [optional] extension: table
) -> int ) -> 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. Returns nil if the emitter does not exist.
``lua ```lua
gfx.particles.set_origin(id: int, origin: vec3 | int) gfx.particles.set_origin(id: int, origin: vec3 | int)
``` ```

View file

@ -58,9 +58,58 @@ not part of Lua syntax.
- quat - array of four numbers - quaternion - quat - array of four numbers - quaternion
- matrix - array of 16 numbers - matrix - 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 ```lua
require "packid:module_name" -- load Lua module from pack-folder/modules/ local module_name = require "contentpack:module_name" -- imports the module
-- no extension included, just name
-- 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.

View file

@ -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. 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 ## Model and physics
```lua ```lua

View file

@ -30,6 +30,9 @@ entities.def_hitbox(id: int) -> vec3
-- Returns entity definition name by index (string ID). -- Returns entity definition name by index (string ID).
entities.def_name(id: int) -> str 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). -- Returns entity definition index by name (integer ID).
entities.def_index(name: str) -> int 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 -- Returns a table of loaded entities based on the passed list of UIDs
entities.get_all(uids: array<int>) -> table entities.get_all(uids: array<int>) -> 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 -- pos - minimal area corner
-- size - area size -- size - area size
entities.get_all_in_box(pos: vec3, size: vec3) -> array<int> entities.get_all_in_box(pos: vec3, size: vec3) -> array<int>
-- 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 -- center - center of the area
-- radius - radius of the area -- radius - radius of the area
entities.get_all_in_radius(center: vec3, radius: number) -> array<int> entities.get_all_in_radius(center: vec3, radius: number) -> array<int>
@ -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. 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. Accordingly, this will affect the presence of the *entity* and *block* fields.
Use `world.raycast(...)` for more detailed configuration.

View file

@ -162,7 +162,19 @@ inventory.get_all_data(
invid: int, invid: int,
-- slot index -- slot index
slot: int, 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 ```lua

View file

@ -70,4 +70,45 @@ world.save_chunk_data(
-- compressed chunk data -- compressed chunk data
data: Bytearray 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<string|number> = 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<string|number> = 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
```
``` ```

View file

@ -38,6 +38,17 @@ entity:set_enabled(name: str, enable: bool)
entity:get_player() -> int or nil 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 ## Built-in components
### Transform ### 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. 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.

View file

@ -52,6 +52,60 @@ yaml.parse(code: str) -> table
Parses a YAML string into a 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
<panel size='400' color='0' interval='1' context='menu'>
<button onclick='menu.page="worlds"'>@Worlds</button>
</panel>
```
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 ## *bjson* library
The library contains functions for working with the binary data exchange format [vcbjson](../../specs/binary_json_spec.md). The library contains functions for working with the binary data exchange format [vcbjson](../../specs/binary_json_spec.md).

View file

@ -1,7 +1,7 @@
# UI properties and methods # UI properties and methods
UI elements in scripts are accessed through a Document instance 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: Example: print the pos property of an element with id: "worlds-panel" to the console:
```lua ```lua

View file

@ -66,6 +66,10 @@
- `relation` - отношение текущего значения к изначальному (x/y) - `relation` - отношение текущего значения к изначальному (x/y)
- `vbar` - вертикальная шкала (используется по-умолчанию) - `vbar` - вертикальная шкала (используется по-умолчанию)
### Имя скрипта - `script-name`
Определяет имя скрипта, содержащего обработчики событий предмета. По-умолчанию соответствует строковому id (имени) предмета.
Свойство обеспечивает возможность использования одного скрипта для нескольких предметов.
## Теги - *tags* ## Теги - *tags*

View file

@ -10,14 +10,14 @@
| lighting | Освещение. | true | | lighting | Освещение. | true |
| collision | Обнаружение столкновений. | true | | collision | Обнаружение столкновений. | true |
| max_distance | Максимальная дистанция от камеры, при которой происходит спавн частиц. | 16.0 | | max_distance | Максимальная дистанция от камеры, при которой происходит спавн частиц. | 16.0 |
| spawn_interval | Интервал спавна частиц в секундах. | 1.0 | | spawn_interval | Интервал спавна частиц в секундах, также задержка перед спавном первой. | 1.0 |
| lifetime | Среднее время жизни частиц в секундах. | 5.0 | | lifetime | Среднее время жизни частиц в секундах. | 5.0 |
| lifetime_spread | Максимальное отклонение времени жизни частицы (от 0.0 до 1.0). | 0.2 | | lifetime_spread | Максимальное отклонение времени жизни частицы (от 0.0 до 1.0). | 0.2 |
| velocity | Начальная линейная скорость частиц. | {0, 0, 0} | | velocity | Начальная линейная скорость частиц. | {0, 0, 0} |
| acceleration | Ускорение частиц. | {0, -16, 0} | | acceleration | Ускорение частиц. | {0, -16, 0} |
| explosion | Сила разлёта частиц при спавне. | {2, 2, 2} | | explosion | Сила разлёта частиц при спавне. | {2, 2, 2} |
| size | Размер частиц. | {0.1, 0.1, 0.1} | | size | Размер частиц. | {0.1, 0.1, 0.1} |
| size_spread | Максимальное отклонение времени размера частиц. | 0.2 | | size_spread | Максимальное отклонение размера частиц. | 0.2 |
| angle_spread | Максимальное отклонение начального угла поворота (от 0 до 1) | 0.0 | | angle_spread | Максимальное отклонение начального угла поворота (от 0 до 1) | 0.0 |
| min_angular_vel | Минимальная угловая скорость (радианы в сек.). Неотрицательное. | 0.0 | | min_angular_vel | Минимальная угловая скорость (радианы в сек.). Неотрицательное. | 0.0 |
| max_angular_vel | Максимальная угловая скорость (радианы в сек.). Неотрицательное. | 0.0 | | max_angular_vel | Максимальная угловая скорость (радианы в сек.). Неотрицательное. | 0.0 |
@ -36,7 +36,8 @@ gfx.particles.emit(
count: int, count: int,
-- таблица настроек частиц -- таблица настроек частиц
preset: table, preset: table,
-- дополнительная таблица настроек частиц -- таблица настроек частиц перезаписывающая значения параметров
-- используется вместо копирования и модификации 'preset'
[опционально] extension: table [опционально] extension: table
) -> int ) -> int
``` ```

View file

@ -60,6 +60,58 @@
- quat - массив из четырех чисел - кватернион - quat - массив из четырех чисел - кватернион
- matrix - массив из 16 чисел - матрица - matrix - массив из 16 чисел - матрица
## Пространства имён
В настоящее время, движок использует иерархию пространств имён, минимизирующую конфликты между отдельными модулями, скриптами, да и паками.
Актуальна следующая структура:
- Глобальное пространство (оно же - _G), являющееся корневым, запись в которое, вне модулей ядра движка, является крайне нежелательным для вашего же времени, что может уйти на лишние часы отладки.
- Пространство пака - создаётся каждый раз при загрузке контента для каждого, включённого в конфигурацию, пака. Это пространство используют модули, а также скрипты предметов и блоков.
- Пространство [компонента](scripting/ecs.md#пользовательские-компоненты).
- Пространство [UI-документа](scripting/ui.md)
- Изолированное пространство имён генератора мира.
## Модули
Модуль - глобальный объект со сроком жизни, ограниченным сроком жизни контента, используемый как для взаимодействия разных паков, так и для вместо глобальных переменных в пределах самого пака.
Модуль должен находиться в `контентпак/modules/**` (допускаются вложенные папки, что нужно будет указывать)
```lua ```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 вернёт кешированный результат.

View file

@ -115,6 +115,8 @@ block.raycast(start: vec3, dir: vec3, max_distance: number, [опциональ
Для результата будет использоваться целевая (dest) таблица вместо создания новой, если указан опциональный аргумент. Для результата будет использоваться целевая (dest) таблица вместо создания новой, если указан опциональный аргумент.
Для более тонкой настройки используйте `world.raycast(...)`.
## Вращение ## Вращение
```lua ```lua

View file

@ -30,6 +30,9 @@ entities.def_name(id: int) -> string
-- Возвращает значение свойства 'hitbox' сущности -- Возвращает значение свойства 'hitbox' сущности
entities.def_hitbox(id: int) -> vec3 entities.def_hitbox(id: int) -> vec3
-- Возвращает true если сущность является осязаемым препятствием.
entities.def_solid(id: int) -> boolean
-- Возвращает индекс определения сущности по имени (числовой ID). -- Возвращает индекс определения сущности по имени (числовой ID).
entities.def_index(name: string) -> int entities.def_index(name: string) -> int
@ -50,12 +53,12 @@ entities.get_all() -> table
-- Возвращает таблицу загруженных сущностей по переданному списку UID -- Возвращает таблицу загруженных сущностей по переданному списку UID
entities.get_all(uids: table<int>) -> table entities.get_all(uids: table<int>) -> table
-- Возвращает список UID сущностей, попадающих в прямоугольную область -- Возвращает список UID сущностей, позиции которых попадают в прямоугольную область
-- pos - минимальный угол области -- pos - минимальный угол области
-- size - размер области -- size - размер области
entities.get_all_in_box(pos: vec3, size: vec3) -> table<int> entities.get_all_in_box(pos: vec3, size: vec3) -> table<int>
-- Возвращает список UID сущностей, попадающих в радиус -- Возвращает список UID сущностей, позиции которых попадают в радиус
-- center - центр области -- center - центр области
-- radius - радиус области -- radius - радиус области
entities.get_all_in_radius(center: vec3, radius: number) -> table<int> entities.get_all_in_radius(center: vec3, radius: number) -> table<int>
@ -70,3 +73,5 @@ entities.raycast(start: vec3, dir: vec3, max_distance: number, ignore: int, [opt
Функция является расширенным вариантом [block.raycast](libblock.md#raycast). Возвращает таблицу с результатами если луч касается блока, либо сущности. Функция является расширенным вариантом [block.raycast](libblock.md#raycast). Возвращает таблицу с результатами если луч касается блока, либо сущности.
Соответственно это повлияет на наличие полей *entity* и *block*. Соответственно это повлияет на наличие полей *entity* и *block*.
Для более тонкой настройки используйте `world.raycast(...)`.

View file

@ -21,7 +21,7 @@ input.get_bindings() -> table<string>
input.get_binding_text(bindname: string) -> string 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) input.set_enabled(bindname: string, flag: boolean)

View file

@ -170,7 +170,19 @@ inventory.get_all_data(
invid: int, invid: int,
-- индекс слота -- индекс слота
slot: int, slot: int,
) -> table ) -> table | nil
-- Устанавливает значения локальных cвойств предмета.
inventory.set_all_data(
-- id инвентаря
invid: int,
-- индекс слота
slot: int,
-- таблица cвойство:значение
fields: table,
-- удалить остальные значения свойств
[опционально] clear: boolean = false
)
``` ```
```lua ```lua

View file

@ -69,4 +69,44 @@ world.save_chunk_data(
-- сжатые данные чанка -- сжатые данные чанка
data: Bytearray data: Bytearray
) )
-- Бросает луч из точки 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<string|number> = nil,
-- исключающий режим фильтрации:
-- при значении (true) filter_entities определяет список игнорируемых
entities_exclusion: bool = false,
[==[ дополнительные параметры (блоки) ]==]
-- id типов блоков используемых для фильтрации
filter_blocks: table<string|number> = 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 если луч не коснулся блока или сущности
``` ```

View file

@ -180,6 +180,17 @@ rig:get_color() -> vec3
rig:set_color(color: vec3) rig:set_color(color: vec3)
``` ```
## Пользовательские компоненты
Компонент описывается в виде скрипта `{пак}/scripts/components/{имя}.lua`.
Компонент будет доступен для использования в описании сущности как `{пак}:{имя}`.
Пример: `core:scripts/components/pathfinding.lua` -> `core:pathfinding`.
Для **каждой** сущности, содержащей компонент, создаётся **отдельный экземпляр** со **своим пространством имен** внутри пространства имён пака.
Глобальные переменные, объявляемые в нём, играют роль публичных полей компонента (как и глобальные функции).
Компоненты сущности указываются через [список "components"](../entity-properties.md#cписок-компонентов---components)
> [!WARNING] > [!WARNING]
> При выходе сущности за пределы зоны прогрузки она удаляется, вызывая события. > При выходе сущности за пределы зоны прогрузки она удаляется, вызывая события.
> При повторном попадании в зону прогрузки сущность спавнится заново. > При повторном попадании в зону прогрузки сущность спавнится заново.
@ -265,3 +276,8 @@ function on_used(playerid: int)
Вызывается при использовании сущности (ПКМ по сущности). ID игрока передается в качестве аргумента. Вызывается при использовании сущности (ПКМ по сущности). ID игрока передается в качестве аргумента.
```lua
function on_player_set(playerid: int)
```
Вызывается при прикреплении сущности к игроку.

View file

@ -156,6 +156,8 @@ math.round(num: number, [опционально] places: number) -> number
-- Возвращает сумму всех принимаемых аргументов. Если в качестве аргумента была передана таблица, метод вернёт сумму всех её элементов. -- Возвращает сумму всех принимаемых аргументов. Если в качестве аргумента была передана таблица, метод вернёт сумму всех её элементов.
math.sum(x: number, ... | t: table) -> number math.sum(x: number, ... | t: table) -> number
-- Возвращает целое число, указывающее знак числа (-1/0/1)
math.sign(x: number) -> int
``` ```
## Расширения для bit ## Расширения для bit

View file

@ -52,6 +52,60 @@ yaml.parse(code: str) -> table
Парсит YAML строку в таблицу. Парсит YAML строку в таблицу.
## Библиотека xml
Библиотека содержит функции для сериализации и десериализации XML:
Далее XML как тип данных обозначает Lua-таблицу со следующей структурой:
- Тег элемента доступен по ключу `#`
- XML-аттрибуты доступны как пары ключ = значение
- Вложенные элементы доступны по индексу
Таблица используется как словарь, так и массив.
Пример:
```xml
<panel size='400' color='0' interval='1' context='menu'>
<button onclick='menu.page="worlds"'>@Worlds</button>
</panel>
```
Эквивалентная 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 ## Библиотека bjson
Библиотека содержит функции для работы с двоичным форматом обмена данными [vcbjson](../../specs/binary_json_spec.md). Библиотека содержит функции для работы с двоичным форматом обмена данными [vcbjson](../../specs/binary_json_spec.md).

View file

@ -1,7 +1,7 @@
# Свойства и методы UI элементов # Свойства и методы UI элементов
Обращение к UI элементов в скриптах произодится через экземпляр Document Обращение к UI элементов в скриптах произодится через экземпляр Document
(переменная document) по id, указанному в xml. (глобальная переменная document) по id, указанному в xml.
Пример: вывод в консоль свойства pos элемента с id: "worlds-panel": Пример: вывод в консоль свойства pos элемента с id: "worlds-panel":
```lua ```lua

View file

@ -1,8 +1,77 @@
local this = {} 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( 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 expected, fact
)) ))
end end

View file

@ -12,11 +12,30 @@ FFI.cdef[[
} bytearray_t; } bytearray_t;
]] ]]
local malloc = FFI.C.malloc
local free = FFI.C.free local free = FFI.C.free
local FFIBytearray local FFIBytearray
local bytearray_type 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 function grow_buffer(self, elems)
local new_capacity = math.ceil(self.capacity / 0.75 + elems) local new_capacity = math.ceil(self.capacity / 0.75 + elems)
local prev = self.bytes local prev = self.bytes

View file

@ -35,3 +35,7 @@ function math.sum(...)
return sum return sum
end end
function math.sign(x)
return (x > 0) and 1 or (x < 0 and -1 or 0)
end

View file

@ -5,4 +5,25 @@ function util.create_demo_world(generator)
app.new_world("demo", "2019", generator or "core:default") app.new_world("demo", "2019", generator or "core:default")
end 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 return util

View file

@ -139,14 +139,16 @@ function get_dir() return mob_dir end
function is_flight() return flight 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 local prev_angle = (vec2.angle({mob_dir[3], mob_dir[1]})) % 360
function on_physics_update(delta) function on_physics_update(delta)
local grounded = body:is_grounded() local grounded = body:is_grounded()
body:set_vdamping(flight) body:set_vdamping(flight)
body:set_gravity_scale(flight and 0.0 or props.gravity_scale)
body:set_linear_damping( body:set_linear_damping(
(flight or not grounded) and props.air_damping or props.ground_damping (flight or not grounded) and props.air_damping or props.ground_damping
) )

View file

@ -58,16 +58,12 @@ local function process_player_inputs(pid, rot, delta)
end end
end end
local prepared = false function on_player_set(pid)
body:set_enabled(false) mob.set_flight(player.is_flight(pid))
mob.set_flight(true) body:set_body_type(player.is_noclip(pid) and "kinematic" or "dynamic")
end
function on_physics_update(delta) function on_physics_update(delta)
if not prepared then
prepared = true
body:set_enabled(true)
end
local pid = entity:get_player() local pid = entity:get_player()
if pid == -1 then if pid == -1 then
return return

View file

@ -312,6 +312,8 @@ entities.get_all = function(uids)
return stdcomp.get_all(uids) return stdcomp.get_all(uids)
end end
end end
world.raycast = entities.__world_raycast
entities.__world_raycast = nil
__vc_scripts_registry = require "core:internal/scripts_registry" __vc_scripts_registry = require "core:internal/scripts_registry"

View file

@ -22,9 +22,26 @@ namespace {
} }
inline double power(double base, int64_t power) { 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<uint64_t>(-(power + 1)) + 1;
} else {
exp = static_cast<uint64_t>(power);
}
double result = 1.0; 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; return result;
} }

View file

@ -49,9 +49,9 @@ static util::Buffer<ubyte> write_to_memory(uint width, uint height, const ubyte*
png_write_info(png_ptr, info_ptr); png_write_info(png_ptr, info_ptr);
auto row = std::make_unique<png_byte[]>(pixsize * width); auto row = std::make_unique<png_byte[]>(pixsize * width);
for (uint y = 0; y < height; y++) { for (int y = height-1; y >= 0; y--) {
for (uint x = 0; x < width; x++) { for (int x = 0; x < width; x++) {
for (uint i = 0; i < pixsize; i++) { for (int i = 0; i < pixsize; i++) {
row[x * pixsize + i] = row[x * pixsize + i] =
(png_byte)data[(y * width + x) * pixsize + i]; (png_byte)data[(y * width + x) * pixsize + i];
} }

View file

@ -528,3 +528,11 @@ std::string xml::stringify(
return ss.str(); 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();
}

View file

@ -119,6 +119,12 @@ namespace xml {
const std::string& indentStr = " " const std::string& indentStr = " "
); );
std::string stringify(
const Node& element,
bool nice = true,
const std::string& indentStr = " "
);
/// @brief Read XML Document from string /// @brief Read XML Document from string
/// @param filename file name will be shown in error messages /// @param filename file name will be shown in error messages
/// @param source xml source code string /// @param source xml source code string

View file

@ -9,8 +9,8 @@ class Font;
struct FontMetrics { struct FontMetrics {
std::optional<std::weak_ptr<Font>> font; std::optional<std::weak_ptr<Font>> font;
int lineHeight; int lineHeight = 0;
int yoffset; int yoffset = 0;
int _glyphInterval = 8; int _glyphInterval = 8;
~FontMetrics(); ~FontMetrics();

View file

@ -1,5 +1,7 @@
#include "ImageData.hpp" #include "ImageData.hpp"
#include "debug/Logger.hpp"
#include <glm/glm.hpp> #include <glm/glm.hpp>
#include <assert.h> #include <assert.h>
#include <stdexcept> #include <stdexcept>
@ -7,6 +9,8 @@
#include <cmath> #include <cmath>
#include <algorithm> #include <algorithm>
static debug::Logger logger("image-data");
ImageData::ImageData(ImageFormat format, uint width, uint height) ImageData::ImageData(ImageFormat format, uint width, uint height)
: format(format), width(width), height(height) { : format(format), width(width), height(height) {
size_t pixsize; size_t pixsize;
@ -106,47 +110,83 @@ std::unique_ptr<ImageData> ImageData::cropped(int x, int y, int width, int heigh
return subImage; return subImage;
} }
static bool clip_line(int& x1, int& y1, int& x2, int& y2, int width, int height) { static bool clip_line(
const int left = 0; int& x0, int& y0, int& x1, int& y1, int width, int height
const int right = width; ) {
const int bottom = 0; if (width <= 0 || height <= 0) {
const int top = height; 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; const int xmin = 0;
int dy = y2 - y1; const int ymin = 0;
const int xmax = width - 1;
const int ymax = height - 1;
float t0 = 0.0f; auto outcode = [&](int x, int y) {
float t1 = 1.0f; int code = INSIDE;
auto clip = [](int p, int q, float& t0, float& t1) { if (x < xmin)
if (p == 0) { code |= LEFT;
return q >= 0; else if (x > xmax)
} code |= RIGHT;
float t = static_cast<float>(q) / p;
if (p < 0) { if (y < ymin)
if (t > t1) return false; code |= BOTTOM;
if (t > t0) t0 = t; else if (y > ymax)
} else { code |= TOP;
if (t < t0) return false;
if (t < t1) t1 = t; return code;
}
return true;
}; };
if (!clip(-dx, x1 - left, t0, t1)) return false; int c0 = outcode(x0, y0);
if (!clip( dx, right - x1, t0, t1)) return false; int c1 = outcode(x1, y1);
if (!clip(-dy, y1 - bottom, t0, t1)) return false;
if (!clip( dy, top - y1, t0, t1)) return false;
if (t1 < 1.0f) { while (true) {
x2 = x1 + static_cast<int>(std::round(t1 * dx)); if (!(c0 | c1)) {
y2 = y1 + static_cast<int>(std::round(t1 * dy)); 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<double>(ymax - y0) / (y1 - y0);
} else if (out & BOTTOM) {
y = ymin;
x = x0 + (x1 - x0) * static_cast<double>(ymin - y0) / (y1 - y0);
} else if (out & RIGHT) {
x = xmax;
y = y0 + (y1 - y0) * static_cast<double>(xmax - x0) / (x1 - x0);
} else { // LEFT
x = xmin;
y = y0 + (y1 - y0) * static_cast<double>(xmin - x0) / (x1 - x0);
}
if (out == c0) {
x0 = std::clamp(static_cast<int>(std::lround(x)), xmin, xmax);
y0 = std::clamp(static_cast<int>(std::lround(y)), ymin, ymax);
c0 = outcode(x0, y0);
} else {
x1 = std::clamp(static_cast<int>(std::lround(x)), xmin, xmax);
y1 = std::clamp(static_cast<int>(std::lround(y)), ymin, ymax);
c1 = outcode(x1, y1);
}
} }
if (t0 > 0.0f) { }
x1 = x1 + static_cast<int>(std::round(t0 * dx));
y1 = y1 + static_cast<int>(std::round(t0 * dy)); static bool is_point_outside(int x, int y, int width, int height) {
} return x < 0 || y < 0 || x >= width || y >= height;
return true;
} }
template<uint channels> template<uint channels>
@ -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 width = image.getWidth();
uint height = image.getHeight(); uint height = image.getHeight();
glm::ivec4 init {x1, y1, x2, y2};
if ((x1 < 0 || x1 >= width || x2 < 0 || x2 >= width || if ((x1 < 0 || x1 >= width || x2 < 0 || x2 >= width ||
y1 < 0 || y1 >= height || y2 < 0 || y2 >= height) && y1 < 0 || y1 >= height || y2 < 0 || y2 >= height) &&
!clip_line(x1, y1, x2, y2, width, height)) { !clip_line(x1, y1, x2, y2, width, height)) {
return; 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<int>(width) - 1);
y1 = glm::clamp(y1, 0, static_cast<int>(height) - 1);
x2 = glm::clamp(x2, 0, static_cast<int>(width) - 1);
y2 = glm::clamp(y2, 0, static_cast<int>(height) - 1);
}
int dx = std::abs(x2 - x1); int dx = std::abs(x2 - x1);
int dy = -std::abs(y2 - y1); int dy = -std::abs(y2 - y1);

View file

@ -22,6 +22,7 @@
#include "voxels/Block.hpp" #include "voxels/Block.hpp"
#include "voxels/Chunk.hpp" #include "voxels/Chunk.hpp"
#include "voxels/Chunks.hpp" #include "voxels/Chunks.hpp"
#include "voxels/blocks_agent.hpp"
#include "window/Camera.hpp" #include "window/Camera.hpp"
#include "world/Level.hpp" #include "world/Level.hpp"
#include "WorldRenderer.hpp" #include "WorldRenderer.hpp"
@ -98,7 +99,7 @@ void Decorator::updateAcoustics(const Camera& camera) {
auto& chunks = *player.chunks; auto& chunks = *player.chunks;
const auto& start = camera.position; const auto& start = camera.position;
float rayLength = 40.0f; float length = 40.0f;
auto& blocks = level.content.getIndices()->blocks; auto& blocks = level.content.getIndices()->blocks;
@ -107,7 +108,7 @@ void Decorator::updateAcoustics(const Camera& camera) {
float averageDistance = 0.0f; float averageDistance = 0.0f;
float averageAbsorption = 0.0f; float averageAbsorption = 0.0f;
float speedOfSound = 300.0f; float speedOfSound = 300.0f;
float minDistance = rayLength; float minDistance = length;
for (int i = 0; i < rays; i++) { for (int i = 0; i < rays; i++) {
float u1 = random.randFloat(); float u1 = random.randFloat();
float u2 = random.randFloat(); float u2 = random.randFloat();
@ -121,12 +122,13 @@ void Decorator::updateAcoustics(const Camera& camera) {
glm::vec3 end; glm::vec3 end;
glm::ivec3 norm; glm::ivec3 norm;
glm::ivec3 iend; 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) { if (vox == nullptr) {
continue; continue;
} }
auto distance = glm::distance(start, end); auto distance = glm::distance(start, end);
if (distance >= rayLength * 0.98f) { if (distance >= length * 0.98f) {
continue; continue;
} }
if (distance < minDistance) { if (distance < minDistance) {
@ -149,7 +151,7 @@ void Decorator::updateAcoustics(const Camera& camera) {
averageAbsorption /= hit; averageAbsorption /= hit;
} }
float decayTime = 20.0f * (averageDistance / rayLength); float decayTime = 20.0f * (averageDistance / length);
float escapeRatio = static_cast<float>(rays - hit) / static_cast<float>(rays); float escapeRatio = static_cast<float>(rays - hit) / static_cast<float>(rays);
acoustics.reverbGain = glm::pow(hit / static_cast<float>(rays), 3.0f); acoustics.reverbGain = glm::pow(hit / static_cast<float>(rays), 3.0f);

View file

@ -207,6 +207,9 @@ uint Label::getLinesNumber() const {
void Label::draw(const DrawContext& pctx, const Assets& assets) { void Label::draw(const DrawContext& pctx, const Assets& assets) {
auto batch = pctx.getBatch2D(); auto batch = pctx.getBatch2D();
auto font = assets.getShared<Font>(fontName); auto font = assets.getShared<Font>(fontName);
if (font == nullptr) {
return;
}
cache.prepare( cache.prepare(
font, font,
font->getMetrics(), font->getMetrics(),

View file

@ -353,7 +353,9 @@ void TextBox::draw(const DrawContext& pctx, const Assets& assets) {
void TextBox::drawBackground(const DrawContext& pctx, const Assets& assets) { void TextBox::drawBackground(const DrawContext& pctx, const Assets& assets) {
auto font = assets.getShared<Font>(label->getFontName()); auto font = assets.getShared<Font>(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(); glm::vec2 pos = calcPos();

View file

@ -55,6 +55,16 @@ public:
return fields; 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 { bool hasFields() const {
return fields != nullptr; return fields != nullptr;
} }

View file

@ -82,7 +82,6 @@ void LevelController::update(float delta, bool pause) {
continue; continue;
} }
player->rotationInterpolation.updateTimer(delta); player->rotationInterpolation.updateTimer(delta);
player->updateEntity();
glm::vec3 position = player->getPosition(); glm::vec3 position = player->getPosition();
player->chunks->configure( player->chunks->configure(
glm::floor(position.x), glm::floor(position.x),
@ -96,9 +95,9 @@ void LevelController::update(float delta, bool pause) {
*player, *player,
player.get() == clientPlayer player.get() == clientPlayer
); );
player->updateEntity();
} }
if (!pause) { if (!pause) {
// update all objects that needed
blocks->update(delta, settings.chunks.padding.get()); blocks->update(delta, settings.chunks.padding.get());
level->entities->update(delta); level->entities->update(delta);
for (const auto& [_, player] : *level->players) { for (const auto& [_, player] : *level->players) {

View file

@ -200,8 +200,13 @@ void CameraControl::update(
auto castRay = [](Player& player, Camera& camera, const glm::vec3& front) auto castRay = [](Player& player, Camera& camera, const glm::vec3& front)
-> glm::vec3 { -> glm::vec3 {
auto blockEnd = player.chunks->rayCastToObstacle(camera.position, front, 3.0f); 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( auto entityRay = player.getLevel().entities->rayCast(
camera.position, front, 3.0f, player.getEntity(), true camera.position, front, 3.0f, raycast
); );
if (entityRay.has_value() && if (entityRay.has_value() &&
entityRay->distance < glm::distance(camera.position, blockEnd)) { entityRay->distance < glm::distance(camera.position, blockEnd)) {
@ -350,7 +355,13 @@ voxel* PlayerController::updateSelection(float maxDistance) {
glm::ivec3 iend; glm::ivec3 iend;
glm::ivec3 norm; glm::ivec3 norm;
voxel* vox = chunks.rayCast( 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) { if (vox) {
maxDistance = glm::distance(camera->position, end); maxDistance = glm::distance(camera->position, end);
@ -358,8 +369,12 @@ voxel* PlayerController::updateSelection(float maxDistance) {
auto prevEntity = selection.entity; auto prevEntity = selection.entity;
selection.entity = ENTITY_NONE; selection.entity = ENTITY_NONE;
selection.actualPosition = iend; selection.actualPosition = iend;
Entities::RaycastSettings raycast {};
raycast.ignoredUid = player.getEntity();
if (auto result = level.entities->rayCast( if (auto result = level.entities->rayCast(
camera->position, camera->front, maxDistance, player.getEntity() camera->position, camera->front, maxDistance, raycast
)) { )) {
selection.entity = result->entity; selection.entity = result->entity;
selection.hitPosition = selection.hitPosition =

View file

@ -53,6 +53,7 @@ extern const luaL_Reg vec3lib[]; // vecn.cpp
extern const luaL_Reg vec4lib[]; // vecn.cpp extern const luaL_Reg vec4lib[]; // vecn.cpp
extern const luaL_Reg weatherlib[]; // gfx.weather extern const luaL_Reg weatherlib[]; // gfx.weather
extern const luaL_Reg worldlib[]; extern const luaL_Reg worldlib[];
extern const luaL_Reg xmllib[];
extern const luaL_Reg yamllib[]; extern const luaL_Reg yamllib[];
// Components // Components

View file

@ -543,13 +543,16 @@ static int l_destruct(lua::State* L) {
} }
static int l_raycast(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 start = lua::tovec<3>(L, 1);
auto dir = lua::tovec<3>(L, 2); auto dir = lua::tovec<3>(L, 2);
auto maxDistance = lua::tonumber(L, 3); auto maxDistance = lua::tonumber(L, 3);
bool includeNonSelectable = false;
blocks_agent::RaycastSettings raycast {};
std::set<blockid_t> filteredBlocks {}; std::set<blockid_t> filteredBlocks {};
raycast.filter = &filteredBlocks;
const int luaStackSize = lua::gettop(L); const int luaStackSize = lua::gettop(L);
if (luaStackSize >= 5) { if (luaStackSize >= 5) {
if (lua::istable(L, 5)) { if (lua::istable(L, 5)) {
@ -568,21 +571,14 @@ static int l_raycast(lua::State* L) {
} }
} }
if (luaStackSize >= 6) { if (luaStackSize >= 6) {
includeNonSelectable = lua::toboolean(L, 6); raycast.includeNonSelectable = lua::toboolean(L, 6);
} }
glm::vec3 end; glm::vec3 end;
glm::ivec3 normal; glm::ivec3 normal;
glm::ivec3 iend; glm::ivec3 iend;
if (auto voxel = blocks_agent::raycast( if (auto voxel = blocks_agent::raycast(
*level.chunks, *level.chunks, start, dir, maxDistance, end, normal, iend, raycast
start,
dir,
maxDistance,
end,
normal,
iend,
filteredBlocks,
includeNonSelectable
)) { )) {
if (luaStackSize >= 4 && !lua::isnil(L, 4)) { if (luaStackSize >= 4 && !lua::isnil(L, 4)) {
lua::pushvalue(L, 4); lua::pushvalue(L, 4);

View file

@ -17,6 +17,8 @@
#include "voxels/blocks_agent.hpp" #include "voxels/blocks_agent.hpp"
#include "window/Camera.hpp" #include "window/Camera.hpp"
#include <set>
using namespace scripting; using namespace scripting;
static const EntityDef* require_entity_def(lua::State* L) { static const EntityDef* require_entity_def(lua::State* L) {
@ -48,6 +50,13 @@ static int l_def_hitbox(lua::State* L) {
return 0; 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) { static int l_defs_count(lua::State* L) {
return lua::pushinteger(L, indices->entities.count()); return lua::pushinteger(L, indices->entities.count());
} }
@ -145,34 +154,59 @@ static int l_get_all_in_radius(lua::State* L) {
return 1; return 1;
} }
static int l_raycast(lua::State* L) { static int l_push_ray(
auto start = lua::tovec<3>(L, 1); lua::State* L,
auto dir = lua::tovec<3>(L, 2); const std::optional<Entities::RaycastResult>& ray,
auto maxDistance = lua::tonumber(L, 3); blockid_t block,
auto ignoreEntityId = lua::tointeger(L, 4); const glm::vec3& start,
bool includeNonSelectable = false; const glm::vec3& dir,
std::set<blockid_t> filteredBlocks {}; const glm::vec3& end,
const int luaStackSize = lua::gettop(L); const glm::ivec3& iend,
if (luaStackSize >= 6) { const glm::ivec3& normal
if (lua::istable(L, 6)) { ) {
int addLen = lua::objlen(L, 6); if (!ray.has_value() && block == BLOCK_VOID) {
for (int i = 0; i < addLen; i++) { return 0;
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");
}
} }
if (luaStackSize >= 7) { if (lua::gettop(L) >= 5 && lua::istable(L, 5)) {
includeNonSelectable = lua::toboolean(L, 6); 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::vec3 end;
glm::ivec3 normal; glm::ivec3 normal;
glm::ivec3 iend; glm::ivec3 iend;
@ -187,61 +221,139 @@ static int l_raycast(lua::State* L) {
end, end,
normal, normal,
iend, iend,
filteredBlocks, voxelRaycast
includeNonSelectable
)) { )) {
maxDistance = glm::distance(start, end); maxDistance = glm::distance(start, end);
block = voxel->id; block = voxel->id;
} }
if (auto ray = auto ray =
level->entities->rayCast(start, dir, maxDistance, ignoreEntityId)) { entities
if (luaStackSize >= 5 && !lua::isnil(L, 5)) { ? level->entities->rayCast(start, dir, maxDistance, entityRaycast)
lua::pushvalue(L, 5); : 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<blockid_t>& 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 { } 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::pop(L);
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;
} }
return 0; }
static void load_entities_filter(lua::State* L, int idx, std::set<entitydefid_t>& 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<blockid_t> filteredBlocks {};
std::set<entitydefid_t> 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<blockid_t> 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) { static int l_reload_component(lua::State* L) {
@ -268,6 +380,7 @@ const luaL_Reg entitylib[] = {
{"def_index", lua::wrap<l_def_index>}, {"def_index", lua::wrap<l_def_index>},
{"def_name", lua::wrap<l_def_name>}, {"def_name", lua::wrap<l_def_name>},
{"def_hitbox", lua::wrap<l_def_hitbox>}, {"def_hitbox", lua::wrap<l_def_hitbox>},
{"def_solid", lua::wrap<l_def_solid>},
{"get_def", lua::wrap<l_get_def>}, {"get_def", lua::wrap<l_get_def>},
{"defs_count", lua::wrap<l_defs_count>}, {"defs_count", lua::wrap<l_defs_count>},
{"spawn", lua::wrap<l_spawn>}, {"spawn", lua::wrap<l_spawn>},
@ -278,6 +391,7 @@ const luaL_Reg entitylib[] = {
{"get_all_in_box", lua::wrap<l_get_all_in_box>}, {"get_all_in_box", lua::wrap<l_get_all_in_box>},
{"get_all_in_radius", lua::wrap<l_get_all_in_radius>}, {"get_all_in_radius", lua::wrap<l_get_all_in_radius>},
{"raycast", lua::wrap<l_raycast>}, {"raycast", lua::wrap<l_raycast>},
{"__world_raycast", lua::wrap<l_world_raycast>},
{"reload_component", lua::wrap<l_reload_component>}, {"reload_component", lua::wrap<l_reload_component>},
{nullptr, nullptr} {nullptr, nullptr}
}; };

View file

@ -237,6 +237,14 @@ static int l_get_all_data(lua::State* L, ItemStack& stack) {
return lua::pushvalue(L, stack.getFields()); 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) { static int l_has_data(lua::State* L, ItemStack& stack) {
auto key = lua::tostring(L, 3); auto key = lua::tostring(L, 3);
if (key == nullptr) { if (key == nullptr) {
@ -267,6 +275,7 @@ const luaL_Reg inventorylib[] = {
{"get_data", lua::wrap<wrap_slot<l_get_data>>}, {"get_data", lua::wrap<wrap_slot<l_get_data>>},
{"set_data", lua::wrap<wrap_slot<l_set_data>>}, {"set_data", lua::wrap<wrap_slot<l_set_data>>},
{"get_all_data", lua::wrap<wrap_slot<l_get_all_data>>}, {"get_all_data", lua::wrap<wrap_slot<l_get_all_data>>},
{"set_all_data", lua::wrap<wrap_slot<l_set_all_data>>},
{"has_data", lua::wrap<wrap_slot<l_has_data>>}, {"has_data", lua::wrap<wrap_slot<l_has_data>>},
{"create", lua::wrap<l_create>}, {"create", lua::wrap<l_create>},
{"remove", lua::wrap<l_remove>}, {"remove", lua::wrap<l_remove>},

View file

@ -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<xml::xmlelement> toxml(lua::State* L) {
lua::getfield(L, TAG_ATTR);
auto tag = lua::require_lstring(L, -1);
lua::pop(L);
auto elem = std::make_unique<xml::xmlelement>(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<l_tostring>},
{"parse", lua::wrap<l_parse>},
{"parse_vcd", lua::wrap<l_parse_vcd>},
{nullptr, nullptr}
};

View file

@ -66,6 +66,7 @@ static void create_libs(State* L, StateType stateType) {
openlib(L, "vec2", vec2lib); openlib(L, "vec2", vec2lib);
openlib(L, "vec3", vec3lib); openlib(L, "vec3", vec3lib);
openlib(L, "vec4", vec4lib); openlib(L, "vec4", vec4lib);
openlib(L, "xml", xmllib);
openlib(L, "yaml", yamllib); openlib(L, "yaml", yamllib);
openlib(L, "__vc_app", applib); openlib(L, "__vc_app", applib);

View file

@ -81,16 +81,17 @@ union RGBA {
uint8_t arr[4]; uint8_t arr[4];
uint32_t rgba; uint32_t rgba;
}; };
static_assert(sizeof(RGBA) == 4);
static RGBA* get_at(const ImageData& data, uint index) { static RGBA* get_at(const ImageData& data, size_t index) {
if (index >= data.getWidth() * data.getHeight()) {
return nullptr;
}
return reinterpret_cast<RGBA*>(data.getData() + index * sizeof(RGBA)); return reinterpret_cast<RGBA*>(data.getData() + index * sizeof(RGBA));
} }
static RGBA* get_at(const ImageData& data, uint x, uint y) { 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<size_t>(y) * data.getWidth() + x);
} }
static RGBA* get_at(State* L, uint x, uint y) { static RGBA* get_at(State* L, uint x, uint y) {

View file

@ -142,6 +142,7 @@ namespace scripting {
void on_entity_grounded(const Entity& entity, float force); void on_entity_grounded(const Entity& entity, float force);
void on_entity_fall(const Entity& entity); void on_entity_fall(const Entity& entity);
void on_entity_save(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_update(int tps, int parts, int part);
void on_entities_physics_update(float delta); void on_entities_physics_update(float delta);
void on_entities_render(float delta); void on_entities_render(float delta);

View file

@ -71,6 +71,7 @@ static void init_component_events(
funcsset.on_aim_off = lua::hasfield(L, "on_aim_off"); funcsset.on_aim_off = lua::hasfield(L, "on_aim_off");
funcsset.on_attacked = lua::hasfield(L, "on_attacked"); funcsset.on_attacked = lua::hasfield(L, "on_attacked");
funcsset.on_used = lua::hasfield(L, "on_used"); funcsset.on_used = lua::hasfield(L, "on_used");
funcsset.on_player_set = lua::hasfield(L, "on_player_set");
lua::pop(L); 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( void scripting::on_sensor_enter(
const Entity& entity, size_t index, entityid_t oid const Entity& entity, size_t index, entityid_t oid
) { ) {

View file

@ -166,8 +166,7 @@ std::optional<Entities::RaycastResult> Entities::rayCast(
glm::vec3 start, glm::vec3 start,
glm::vec3 dir, glm::vec3 dir,
float maxDistance, float maxDistance,
entityid_t ignore, const RaycastSettings& settings
bool solidOnly
) { ) {
Ray ray(start, dir); Ray ray(start, dir);
auto view = registry->view<EntityId, Transform, Rigidbody>(); auto view = registry->view<EntityId, Transform, Rigidbody>();
@ -176,10 +175,18 @@ std::optional<Entities::RaycastResult> Entities::rayCast(
glm::ivec3 foundNormal; glm::ivec3 foundNormal;
for (auto [entity, eid, transform, body] : view.each()) { 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; 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; glm::ivec3 normal;
double distance; double distance;
if (ray.intersectAABB( if (ray.intersectAABB(

View file

@ -5,6 +5,7 @@
#include <optional> #include <optional>
#include <vector> #include <vector>
#include "constants.hpp"
#include "physics/Hitbox.hpp" #include "physics/Hitbox.hpp"
#include "Transform.hpp" #include "Transform.hpp"
#include "Rigidbody.hpp" #include "Rigidbody.hpp"
@ -46,12 +47,6 @@ class Entities final {
); );
void preparePhysics(float delta); void preparePhysics(float delta);
public: public:
struct RaycastResult {
entityid_t entity;
glm::ivec3 normal;
float distance;
};
Entities(Level& level); Entities(Level& level);
~Entities(); ~Entities();
@ -82,20 +77,33 @@ public:
std::optional<Entity> get(entityid_t id); std::optional<Entity> 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<entitydefid_t>* entitiesFilter = nullptr;
};
struct RaycastResult {
entityid_t entity;
glm::ivec3 normal;
float distance;
};
/// @brief Entities raycast. No blocks check included, use combined with /// @brief Entities raycast. No blocks check included, use combined with
/// Chunks.rayCast /// Chunks.rayCast
/// @param start Ray start /// @param start Ray start
/// @param dir Ray direction normalized vector /// @param dir Ray direction normalized vector
/// @param maxDistance Max ray length /// @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 /// @return An optional structure containing entity, normal and distance
std::optional<RaycastResult> rayCast( std::optional<RaycastResult> rayCast(
glm::vec3 start, glm::vec3 start,
glm::vec3 dir, glm::vec3 dir,
float maxDistance, float maxDistance,
entityid_t ignore = -1, const RaycastSettings& settings
bool solidOnly = false
); );
void loadEntities(dv::value map); void loadEntities(dv::value map);

View file

@ -113,6 +113,11 @@ int64_t Entity::getPlayer() const {
} }
void Entity::setPlayer(int64_t id) { void Entity::setPlayer(int64_t id) {
registry.get<EntityId>(entity).player = id; auto& eid = registry.get<EntityId>(entity);
if (eid.player == id) {
return;
}
eid.player = id;
scripting::on_entity_player_set(*this, id);
} }

View file

@ -81,7 +81,7 @@ void Player::updateEntity() {
} }
} }
Hitbox* Player::getHitbox() { Hitbox* Player::getHitbox() const {
if (auto entity = level.entities->get(eid)) { if (auto entity = level.entities->get(eid)) {
return &entity->getRigidbody().hitbox; return &entity->getRigidbody().hitbox;
} }

View file

@ -135,7 +135,7 @@ public:
bool isCurrentCameraBuiltin() const; bool isCurrentCameraBuiltin() const;
Hitbox* getHitbox(); Hitbox* getHitbox() const;
void setSpawnPoint(glm::vec3 point); void setSpawnPoint(glm::vec3 point);
glm::vec3 getSpawnPoint() const; glm::vec3 getSpawnPoint() const;

View file

@ -17,6 +17,7 @@ struct EntityFuncsSet {
bool on_aim_off; bool on_aim_off;
bool on_attacked; bool on_attacked;
bool on_used; bool on_used;
bool on_player_set;
}; };
struct UserComponent { struct UserComponent {

View file

@ -11,8 +11,10 @@
namespace util { namespace util {
template<typename T> template<typename T>
inline T read_int_le(const uint8_t* src, size_t offset=0) { inline T read_int_le(const uint8_t* src) {
return dataio::le2h(*(reinterpret_cast<const T*>(src) + offset)); T tmp;
std::memcpy(&tmp, src, sizeof(T));
return dataio::le2h(tmp);
} }
/// @brief Simple heap implementation for memory-optimal sparse array of /// @brief Simple heap implementation for memory-optimal sparse array of
@ -87,8 +89,7 @@ namespace util {
std::memset(found, 0, entrySize); std::memset(found, 0, entrySize);
return found; return found;
} }
this->free(found); free(found);
return allocate(index, size);
} }
for (size_t i = 0; i < entriesCount; i++) { for (size_t i = 0; i < entriesCount; i++) {
auto data = buffer.data() + offset; auto data = buffer.data() + offset;
@ -110,9 +111,12 @@ namespace util {
entriesCount++; entriesCount++;
auto data = buffer.data() + offset; auto data = buffer.data() + offset;
*reinterpret_cast<Tindex*>(data) = dataio::h2le(index);
Tindex indexTmp = dataio::h2le(index);
Tsize sizeTmp = dataio::h2le(size);
std::memcpy(data, &indexTmp, sizeof(Tindex));
data += sizeof(Tindex); data += sizeof(Tindex);
*reinterpret_cast<Tsize*>(data) = dataio::h2le(size); std::memcpy(data, &sizeTmp, sizeof(Tsize));
return data + sizeof(Tsize); return data + sizeof(Tsize);
} }
@ -122,7 +126,7 @@ namespace util {
if (ptr == nullptr) { if (ptr == nullptr) {
return 0; return 0;
} }
return read_int_le<Tsize>(ptr, -1); return read_int_le<Tsize>(ptr - sizeof(Tsize));
} }
/// @return number of entries /// @return number of entries
@ -147,7 +151,8 @@ namespace util {
ubyte* dst = out.data(); ubyte* dst = out.data();
const ubyte* src = buffer.data(); const ubyte* src = buffer.data();
*reinterpret_cast<Tindex*>(dst) = dataio::h2le(entriesCount); Tindex countTmp = dataio::h2le(entriesCount);
std::memcpy(dst, &countTmp, sizeof(Tindex));
dst += sizeof(Tindex); dst += sizeof(Tindex);
std::memcpy(dst, src, buffer.size()); std::memcpy(dst, src, buffer.size());
@ -172,7 +177,7 @@ namespace util {
) : buffer(buffer), index(index), offset(offset) {} ) : buffer(buffer), index(index), offset(offset) {}
Tsize size() const { Tsize size() const {
return read_int_le<Tsize>(buffer.data() + offset, -1); return read_int_le<Tsize>(buffer.data() + offset - sizeof(Tsize));
} }
bool operator!=(const const_iterator& o) const { bool operator!=(const const_iterator& o) const {

View file

@ -215,11 +215,10 @@ voxel* Chunks::rayCast(
glm::vec3& end, glm::vec3& end,
glm::ivec3& norm, glm::ivec3& norm,
glm::ivec3& iend, glm::ivec3& iend,
std::set<blockid_t> filter, const blocks_agent::RaycastSettings& settings
bool includeNonSelectable
) const { ) const {
return blocks_agent::raycast( return blocks_agent::raycast(
*this, start, dir, maxDist, end, norm, iend, std::move(filter), includeNonSelectable *this, start, dir, maxDist, end, norm, iend, settings
); );
} }

View file

@ -24,6 +24,10 @@ class VoxelsVolume;
template <int w, int h, int d> class StaticVoxelsVolume; template <int w, int h, int d> class StaticVoxelsVolume;
namespace blocks_agent {
struct RaycastSettings;
}
/// Player-centred chunks matrix /// Player-centred chunks matrix
class Chunks { class Chunks {
LevelEvents* events; LevelEvents* events;
@ -112,8 +116,7 @@ public:
glm::vec3& end, glm::vec3& end,
glm::ivec3& norm, glm::ivec3& norm,
glm::ivec3& iend, glm::ivec3& iend,
std::set<blockid_t> filter = {}, const blocks_agent::RaycastSettings& settings
bool includeNonSelectable = false
) const; ) const;
glm::vec3 rayCastToObstacle( glm::vec3 rayCastToObstacle(

View file

@ -223,8 +223,7 @@ static inline voxel* raycast_blocks(
glm::vec3& end, glm::vec3& end,
glm::ivec3& norm, glm::ivec3& norm,
glm::ivec3& iend, glm::ivec3& iend,
std::set<blockid_t> filter, const RaycastSettings& settings
bool includeNonSelectable
) { ) {
const auto& blocks = chunks.getContentIndices().blocks; const auto& blocks = chunks.getContentIndices().blocks;
float px = start.x; float px = start.x;
@ -240,9 +239,8 @@ static inline voxel* raycast_blocks(
int iy = std::floor(py); int iy = std::floor(py);
int iz = std::floor(pz); int iz = std::floor(pz);
int stepx = (dx > 0.0f) ? 1 : -1; glm::ivec3 step {
int stepy = (dy > 0.0f) ? 1 : -1; (dx > 0.0f) ? 1 : -1, (dy > 0.0f) ? 1 : -1, (dz > 0.0f) ? 1 : -1};
int stepz = (dz > 0.0f) ? 1 : -1;
constexpr float infinity = std::numeric_limits<float>::infinity(); constexpr float infinity = std::numeric_limits<float>::infinity();
constexpr float epsilon = 1e-6f; // 0.000001 constexpr float epsilon = 1e-6f; // 0.000001
@ -250,15 +248,16 @@ static inline voxel* raycast_blocks(
float tyDelta = (std::fabs(dy) < epsilon) ? infinity : std::fabs(1.0f / dy); 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 tzDelta = (std::fabs(dz) < epsilon) ? infinity : std::fabs(1.0f / dz);
float xdist = (stepx > 0) ? (ix + 1 - px) : (px - ix); float xdist = (step.x > 0) ? (ix + 1 - px) : (px - ix);
float ydist = (stepy > 0) ? (iy + 1 - py) : (py - iy); float ydist = (step.y > 0) ? (iy + 1 - py) : (py - iy);
float zdist = (stepz > 0) ? (iz + 1 - pz) : (pz - iz); float zdist = (step.z > 0) ? (iz + 1 - pz) : (pz - iz);
float txMax = (txDelta < infinity) ? txDelta * xdist : infinity; float txMax = (txDelta < infinity) ? txDelta * xdist : infinity;
float tyMax = (tyDelta < infinity) ? tyDelta * ydist : infinity; float tyMax = (tyDelta < infinity) ? tyDelta * ydist : infinity;
float tzMax = (tzDelta < infinity) ? tzDelta * zdist : infinity; float tzMax = (tzDelta < infinity) ? tzDelta * zdist : infinity;
int steppedIndex = -1; int steppedIndex = -1;
bool includeNonSelectable = settings.includeNonSelectable;
while (t <= maxDist) { while (t <= maxDist) {
voxel* voxel = get(chunks, ix, iy, iz); voxel* voxel = get(chunks, ix, iy, iz);
@ -266,15 +265,14 @@ static inline voxel* raycast_blocks(
return nullptr; return nullptr;
} }
auto filter = settings.filter;
const auto& def = blocks.require(voxel->id); const auto& def = blocks.require(voxel->id);
if (voxel->id != BLOCK_AIR && (def.selectable || includeNonSelectable) && if (voxel->id != BLOCK_AIR &&
(filter.empty() || filter.find(def.rt.id) == filter.end())) { (def.selectable || includeNonSelectable) &&
end.x = px + t * dx; ((filter == nullptr || filter->find(def.rt.id) == filter->end()) ==
end.y = py + t * dy; settings.blocksFilterExcludeMode)) {
end.z = pz + t * dz; end = start + t * dir;
iend.x = ix; iend = {ix, iy, iz};
iend.y = iy;
iend.z = iz;
if (!def.rt.solid) { if (!def.rt.solid) {
const std::vector<AABB>& hitboxes = const std::vector<AABB>& hitboxes =
@ -309,51 +307,43 @@ static inline voxel* raycast_blocks(
if (hit) return voxel; if (hit) return voxel;
} else { } else {
iend.x = ix; iend = {ix, iy, iz};
iend.y = iy; norm = {0, 0, 0};
iend.z = iz; if (steppedIndex != -1) {
norm[steppedIndex] = -step[steppedIndex];
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;
return voxel; return voxel;
} }
} }
if (txMax < tyMax) { if (txMax < tyMax) {
if (txMax < tzMax) { if (txMax < tzMax) {
ix += stepx; ix += step.x;
t = txMax; t = txMax;
txMax += txDelta; txMax += txDelta;
steppedIndex = 0; steppedIndex = 0;
} else { } else {
iz += stepz; iz += step.z;
t = tzMax; t = tzMax;
tzMax += tzDelta; tzMax += tzDelta;
steppedIndex = 2; steppedIndex = 2;
} }
} else { } else {
if (tyMax < tzMax) { if (tyMax < tzMax) {
iy += stepy; iy += step.y;
t = tyMax; t = tyMax;
tyMax += tyDelta; tyMax += tyDelta;
steppedIndex = 1; steppedIndex = 1;
} else { } else {
iz += stepz; iz += step.z;
t = tzMax; t = tzMax;
tzMax += tzDelta; tzMax += tzDelta;
steppedIndex = 2; steppedIndex = 2;
} }
} }
} }
iend.x = ix; iend = {ix, iy, iz};
iend.y = iy; end = start + t * dir;
iend.z = iz; norm = {0, 0, 0};
end.x = px + t * dx;
end.y = py + t * dy;
end.z = pz + t * dz;
norm.x = norm.y = norm.z = 0;
return nullptr; return nullptr;
} }
@ -365,10 +355,9 @@ voxel* blocks_agent::raycast(
glm::vec3& end, glm::vec3& end,
glm::ivec3& norm, glm::ivec3& norm,
glm::ivec3& iend, glm::ivec3& iend,
std::set<blockid_t> filter, const RaycastSettings& settings
bool includeNonSelectable
) { ) {
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( voxel* blocks_agent::raycast(
@ -379,10 +368,9 @@ voxel* blocks_agent::raycast(
glm::vec3& end, glm::vec3& end,
glm::ivec3& norm, glm::ivec3& norm,
glm::ivec3& iend, glm::ivec3& iend,
std::set<blockid_t> filter, const RaycastSettings& settings
bool includeNonSelectable
) { ) {
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 // reduce nesting on next modification

View file

@ -412,6 +412,12 @@ inline void set_rotation(
} }
} }
struct RaycastSettings {
const std::set<blockid_t>* filter = nullptr;
bool blocksFilterExcludeMode = true;
bool includeNonSelectable = false;
};
/// @brief Cast ray to a selectable block with filter based on id. /// @brief Cast ray to a selectable block with filter based on id.
/// @param chunks chunks matrix /// @param chunks chunks matrix
/// @param start ray start position /// @param start ray start position
@ -431,8 +437,7 @@ voxel* raycast(
glm::vec3& end, glm::vec3& end,
glm::ivec3& norm, glm::ivec3& norm,
glm::ivec3& iend, glm::ivec3& iend,
std::set<blockid_t> filter, const RaycastSettings& settings
bool includeNonSelectable
); );
/// @brief Cast ray to a selectable block with filter based on id. /// @brief Cast ray to a selectable block with filter based on id.
@ -454,8 +459,7 @@ voxel* raycast(
glm::vec3& end, glm::vec3& end,
glm::ivec3& norm, glm::ivec3& norm,
glm::ivec3& iend, glm::ivec3& iend,
std::set<blockid_t> filter, const RaycastSettings& settings
bool includeNonSelectable
); );
void get_voxels( void get_voxels(

View file

@ -33,6 +33,7 @@ static void init_gl_extensions_list() {
} }
} }
[[maybe_unused]]
static bool is_gl_extension_supported(const char *extension) { static bool is_gl_extension_supported(const char *extension) {
if (!extension || !*extension) { if (!extension || !*extension) {
return false; 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(); return supported_gl_extensions.find(extension) != supported_gl_extensions.end();
} }
#ifndef __APPLE__
static const char* gl_error_name(int error) { static const char* gl_error_name(int error) {
switch (error) { switch (error) {
case GL_DEBUG_TYPE_ERROR: return "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) << ":" logger.warning() << "GL:" << gl_error_name(type) << ":"
<< gl_severity_name(severity) << ": " << message; << gl_severity_name(severity) << ": " << message;
} }
#endif
static bool initialize_gl(int width, int height) { static bool initialize_gl(int width, int height) {
glewExperimental = GL_TRUE; glewExperimental = GL_TRUE;

View file

@ -8,6 +8,8 @@ TEST(JSON, EncodeDecode) {
const int bytesSize = 20; const int bytesSize = 20;
const int year = 2019; const int year = 2019;
const float score = 3.141592; const float score = 3.141592;
const double distance = 1.23e45;
const double epsilon = 1e-6;
const bool visible = true; const bool visible = true;
dv::objects::Bytes srcBytes(bytesSize); dv::objects::Bytes srcBytes(bytesSize);
for (int i = 0; i < bytesSize; i ++) { for (int i = 0; i < bytesSize; i ++) {
@ -20,6 +22,8 @@ TEST(JSON, EncodeDecode) {
object["name"] = name; object["name"] = name;
object["year"] = year; object["year"] = year;
object["score"] = score; object["score"] = score;
object["distance"] = distance;
object["epsilon"] = epsilon;
object["visible"] = visible; object["visible"] = visible;
object["data"] = srcBytes; object["data"] = srcBytes;
@ -30,6 +34,8 @@ TEST(JSON, EncodeDecode) {
EXPECT_EQ(object["name"].asString(), name); EXPECT_EQ(object["name"].asString(), name);
EXPECT_EQ(object["year"].asInteger(), year); EXPECT_EQ(object["year"].asInteger(), year);
EXPECT_FLOAT_EQ(object["score"].asNumber(), score); 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); EXPECT_EQ(object["visible"].asBoolean(), visible);
auto b64string = object["data"].asString(); auto b64string = object["data"].asString();

View file

@ -8,9 +8,13 @@ TEST(SmallHeap, Allocation) {
auto index = 0; auto index = 0;
auto size = 4; auto size = 4;
std::cout << "step 1" << std::endl;
SmallHeap<uint16_t, uint8_t> map; SmallHeap<uint16_t, uint8_t> map;
std::cout << "step 2" << std::endl;
auto ptr = map.allocate(index, size); 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); EXPECT_EQ(map.sizeOf(ptr), size);
std::cout << "step 4" << std::endl;
EXPECT_EQ(ptr, map.find(index)); EXPECT_EQ(ptr, map.find(index));
} }