Merge branch 'main' into environments

This commit is contained in:
MihailRis 2026-07-26 23:56:00 +03:00
commit f9bc401236
134 changed files with 3186 additions and 883 deletions

View file

@ -15,6 +15,22 @@
"VOXELENGINE_BUILD_TESTS": "ON" "VOXELENGINE_BUILD_TESTS": "ON"
} }
}, },
{
"name": "default-ninja-mingw-windows",
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
},
"generator": "Ninja Multi-Config",
"binaryDir": "${sourceDir}/build-mingw",
"cacheVariables": {
"CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake",
"VCPKG_HOST_TRIPLET": "x64-mingw-static",
"VCPKG_TARGET_TRIPLET": "x64-mingw-static",
"VOXELENGINE_BUILD_TESTS": "ON"
}
},
{ {
"name": "default-ninja-gnu-linux", "name": "default-ninja-gnu-linux",
"condition": { "condition": {
@ -60,6 +76,11 @@
"configurePreset": "default-vs-msvc-windows", "configurePreset": "default-vs-msvc-windows",
"configuration": "Debug" "configuration": "Debug"
}, },
{
"name": "default-ninja-mingw-windows",
"configurePreset": "default-ninja-mingw-windows",
"configuration": "Debug"
},
{ {
"name": "default-ninja-gnu-linux", "name": "default-ninja-gnu-linux",
"configurePreset": "default-ninja-gnu-linux", "configurePreset": "default-ninja-gnu-linux",

View file

@ -152,6 +152,26 @@ cmake --build --preset default-vs-msvc-windows
> [!NOTE] > [!NOTE]
> Make sure your `CMakeUserPresets.json` (if used) contains the correct `VCPKG_ROOT` path. > Make sure your `CMakeUserPresets.json` (if used) contains the correct `VCPKG_ROOT` path.
---
### Building with CLion (MinGW / Ninja)
> [!NOTE]
> Requirements: **vcpkg**, **Git**, and **CLion** (bundled with CMake, Ninja and MinGW toolchain).
> [!WARNING]
> Avoid installing tools in paths with spaces (e.g. `Program Files`).
> This may break MinGW builds (especially `windres`).
Install **vcpkg** manually as described in the Visual Studio section.
#### Configure CLion
Open the project in CLion and configure CMake profile:
```text
Settings → Build, Execution, Deployment → CMake
```
Reload CMake project and build the engine.
--- ---
## Build using Docker ## Build using Docker

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)

34
dev/tests/io_stream.lua Normal file
View file

@ -0,0 +1,34 @@
local device = file.create_memory_device()
file.write(device..":test.txt", "Hello\nWorld")
file.write_bytes(device..":test.bin", Bytearray({20, 30, 100, 200, 255}))
local stream = file.open(device..":test.txt", 'r')
local line1 = stream:read_line()
local line2 = stream:read_line()
assert(line1 == "Hello")
assert(line2 == "World")
local stream2 = file.open(device..":test.bin", 'rb')
local data = stream2:read(5)
assert(data[1] == 20)
assert(data[2] == 30)
assert(data[3] == 100)
assert(data[4] == 200)
assert(data[5] == 255)
local stream3 = file.open(device..":test.txt", 'r')
assert(stream3:is_alive())
stream3:close()
assert(not stream3:is_alive())
local stream4 = file.open(device..":test.txt", 'r')
stream4:seek('b', 6)
local line = stream4:read_line()
assert(line == "World")
local stream5 = file.open(device..":test.txt", 'r')
stream5:seek('e', 0)
local pos = stream5:tell()
assert(pos == 11)

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

@ -4,14 +4,15 @@
The appearance of 3D text, is configured via a table, like [particles](particles.md). All fields are optional. The appearance of 3D text, is configured via a table, like [particles](particles.md). All fields are optional.
| Field | Description | Default | | Field | Description | Default |
| --------------- | ---------------------------- | ---------------- | | --------------- | ---------------------------------------- | ---------------- |
| display | Display format | static_billboard | | display | Display format | static_billboard |
| color | Text color | {1, 1, 1, 1} | | color | Text color | {1, 1, 1, 1} |
| scale | Text scale | 1 | | scale | Text scale | 1 |
| render_distance | Text rendering distance | 32 | | render_distance | Text rendering distance | 32 |
| xray_opacity | Visibility through obstacles | 0 | | xray_opacity | Visibility through obstacles | 0 |
| perspective | Perspective coefficient | 1 | | perspective | Perspective coefficient | 1 |
| font | Font. If absent, the default one is used | "" |
Available display formats: Available display formats:

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

@ -42,6 +42,7 @@ Subsections:
- [utf8](scripting/builtins/libutf8.md) - [utf8](scripting/builtins/libutf8.md)
- [vec2, vec3, vec4](scripting/builtins/libvecn.md) - [vec2, vec3, vec4](scripting/builtins/libvecn.md)
- [world](scripting/builtins/libworld.md) - [world](scripting/builtins/libworld.md)
- [Extensions for standard libraries](scripting/extensions.md)
- [Module core:bit_converter](scripting/modules/core_bit_converter.md) - [Module core:bit_converter](scripting/modules/core_bit_converter.md)
- [Module core:data_buffer](scripting/modules/core_data_buffer.md) - [Module core:data_buffer](scripting/modules/core_data_buffer.md)
- [Module core:vector2, core:vector3](scripting/modules/core_vector2_vector3.md) - [Module core:vector2, core:vector3](scripting/modules/core_vector2_vector3.md)
@ -58,9 +59,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

@ -0,0 +1,204 @@
# Standard Library Extensions
The **stdmin.lua** kernel script defines functions that extend and complement some of the standard **Lua** libraries.
## Contents:
- [table extensions](#table-extensions)
- [string extensions](#string-extensions)
- [math extensions](#math-extensions)
- [bit extensions](#bit-extensions)
- [additional global functions](#additional-global-functions)
## Table Extensions
```lua
-- Creates and returns a copy of the given table by creating a new one and copying all elements from the given table into it.
table.copy(t: table) -> table
-- The deep copy function creates a full copy of the source table, including all its subtables.
table.deep_copy(t: table) -> table
-- Returns the number of pairs in the given table.
table.count_pairs(t: table) -> int
-- Returns one element from the given table at a random position.
table.random(t: table) -> any
-- Returns true if x is contained in t.
table.has(t: table, x: any) -> boolean
-- Returns the index of x in t. If the given object is not contained in the table, the function returns -1.
table.index(t: table, x: any) -> int
-- Removes element x from t.
table.remove_value(t: table, x: any)
-- Shuffles values ​​in the table.
table.shuffle(t: table) -> table
-- Adds values ​​from table t2 to table t1. If table t2 contains a key from t1, the key's value will not be changed.
table.merge(t1: table, t2: table) -> table
-- Iterates through the table and applies a func to all its elements, returning the new value of the element.
table.map(t: table, func: function(indx, value)) -> table
-- Iterates through the table using a func that returns true if the element should be kept and false if it should be deleted.
table.filter(t: table, func: function(indx, value)) -> table
-- Allows you to safely retrieve the value for the specified key. If the key exists in the table, the method will return its value.
-- If the key is missing, the method will set it to the default value and return it.
table.set_default(t: table, key: int | string, default: any) -> any
-- Returns a "flattened" version of the original table.
table.flat(t: table) -> table
-- Returns a deep "flat" version of the original table.
table.deep_flat(t: table) -> table
-- Returns a truncated version of the table from index start to index stop, inclusive. Key-value pairs
-- are not preserved in the new table. For nil values, the value starts at 1 and ends at #arr, respectively.
table.sub(arr: table, start: number | nil, stop: number | nil) -> table
-- Adds a value to the table only if it was not originally there.
table.insert_unique(t: table, val: any)
table.insert_unique(t: table, pos: int, val: any)
-- Returns a table containing all keys of the given table, including numeric ones. table.keys(t: table) -> table
-- Adds all key-value pairs from table extension to table t. If a key from t is present in extension, its value will be overwritten.
table.extend(t: table, extension: table) -> table
-- Converts the passed table to a string.
table.tostring(t: table) -> string
```
## String extensions
It's important to note that all of the functions listed below that extend **string** can be used as meta-methods on string instances, i.e.:
```lua
local str = "ABA str BAB"
if str:starts_with("ABA") and str:ends_with("BAB") then
print(str:replace("BA", "DC"))
end
```
```lua
-- Splits the string into parts based on the specified separator/expression and returns the result as a table of strings. If withpattern is true, the separator parameter will be evaluated as a regular expression. string.explode(separator: string, str: string, withpattern: boolean) -> table<string>
-- Splits the string into parts based on the specified delimiter and returns the result as a table of strings.
string.split(str: string, delimiter: string) -> table<string>
-- Escapes special characters in the string, such as `()[]+-.$%^?*`, into `%character` format. The `NUL` character (`\0`) will be converted to `%z`.
string.pattern_safe(str: string) -> string
-- Splits seconds into hours, minutes, and milliseconds and formats them using the following parameter order: `minutes, seconds, milliseconds`, and then returns the result. If format is not specified, it returns a table where:
-- h - hours,
-- m - minutes,
-- s - seconds,
-- ms - milliseconds.
string.formatted_time(seconds: number, format: string) -> string | table
-- Replaces all substrings in str equal to tofind with toreplace and returns a string with all the modified substrings.
string.replace(str: string, tofind: string, toreplace: string) -> string
-- Removes all characters equal to char from the string from the left and right ends and returns the result.
-- If the char parameter is undefined, all empty characters will be selected.
string.trim(str: string, char: string) -> string
-- Removes all characters equal to char from the string from the left end and returns the result.
-- If the char parameter is undefined, all empty characters will be selected.
string.trim_left(str: string, char: string) -> string
-- Removes all characters equal to char from the right end of the string and returns the result.
-- If the char parameter is undefined, all empty characters will be selected.
string.trim_right(str: string, char: string) -> string
-- Returns true if the string begins with the substring start.
string.starts_with(str: string, start: string) -> boolean
-- Returns true if the string ends with the substring endStr.
string.ends_with(str: string, endStr: string) -> boolean
-- The string.lower and string.upper functions are also overridden by utf8.lower and utf8.upper.
-- Escapes a string. It is an alias for utf8.escape.
string.escape(str: string) -> string
-- Escapes special XML characters. An alias for utf8.escape_xml.
string.escape_xml(text: string) -> string
-- Adds a char to the left and right of the string until its size equals size.
-- By default, char is equal to the space character.
string.pad(str: string, size: int, char: string) -> string
-- Adds a char to the left of the string until its size equals size.
-- By default, char is equal to the space character.
string.left_pad(str: string, size: int, char: string) -> string
-- Adds a char to the right of the string until its size equals size.
-- By default, char is equal to the space character.
string.right_pad(str: string, size: int, char: string) -> string
```
## Math extensions
```lua
-- Returns _in if it is in the range low <= _in <= high
-- Otherwise, returns the boundary to which _in is closest.
math.clamp(_in: number, low: number, high: number) -> number
-- Returns a random fractional number in the range low to high.
math.rand(low: number, high: number) -> number
-- Returns the normalized value of num relative to conf.
math.normalize(num: number, [optional] conf: number) -> number
-- Returns the rounded value of num to the specified number of decimal places.
math.round(num: number, [optional] places: number) -> number
-- Returns the sum of all received arguments. If a table was passed as an argument, the method will return the sum of all its elements.
math.sum(x: number, ... | t: table) -> number
```
## Bit extensions
```lua
-- Common arguments:
-- * expr: A string containing a bitwise expression, conforming to the Lua 5.3 bitwise operations format
-- * args: A list of names of the expression arguments. If nil, the list is automatically generated based on the detected identifiers.
- Compiles the function to perform bitwise operations
-- * asFunction: If true, returns the function; otherwise, returns a string of function code
bit.compile(expr: string, args: table | nil, asFunction: boolean=true) -> function | string
-- Compiles the function to perform bitwise operations and executes it in place
-- * ...: Values ​​to be passed to the compiled function. bit.execute(expr: string, args: table | nil, ...) -> number
```
## Additional Global Functions
This script also defines other global functions that are available for use. Their list is below.
```lua
-- Returns true if the passed table is an array, that is, if each key is an integer greater than or equal to one
-- and if each key follows the previous one.
is_array(x: table) -> boolean
-- Splits the path into two parts and returns them: the entry point and the file path.
parse_path(path: string) -> string, string
-- Calls the function func iters times, passing it the arguments ..., and then prints to the console the time in microseconds that has elapsed
-- since the call to timeit. timeit(iters: int, func: function, ...)
-- Causes the coroutine to pause until the number of seconds specified in timesec has elapsed.
-- The function can only be used inside a coroutine.
sleep(timesec: number)
-- Waits for the passed coroutine to complete, returning the control flow. The function can only be used inside a coroutine.
-- Returns values ​​similar to those returned by pcall.
await(co: coroutine) -> result, error
-- A constant storing the PID of the current engine instance.
os.pid -> number
```

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

@ -12,6 +12,7 @@
| render_distance | Дистанция отрисовки текста | 32 | | render_distance | Дистанция отрисовки текста | 32 |
| xray_opacity | Коэффициент видимости через препятствия (просвечивание) | 0 | | xray_opacity | Коэффициент видимости через препятствия (просвечивание) | 0 |
| perspective | Коэффициент перспективы | 1 | | perspective | Коэффициент перспективы | 1 |
| font | Шрифт. При отсутствии используется основной | "" |
Доступные форматы отображения: Доступные форматы отображения:

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
@ -209,4 +211,4 @@ block.get_field(
name: string, name: string,
[опционально] index: int = 0 [опционально] index: int = 0
) -> any | nil ) -> any | nil
``` ```

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>
@ -69,4 +72,6 @@ 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)
@ -74,4 +74,4 @@ input.add_callback("key:escape", function ()
print("NO") print("NO")
return true -- предотвращает вызов назначенных ранее функций return true -- предотвращает вызов назначенных ранее функций
end, document.root) end, document.root)
``` ```

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
@ -222,4 +234,4 @@ inventory.use(
-- индекс слота -- индекс слота
slot: int slot: int
) )
``` ```

View file

@ -92,6 +92,11 @@ socket:recv_async(
[опционально] usetable: boolean=false [опционально] usetable: boolean=false
) -> nil|table|Bytearray ) -> nil|table|Bytearray
-- Оборачивает сокет в io_stream (см. ../io_stream.md)
socket:as_stream(
[опционально] binary_mode: boolean=true
) -> io_stream
-- Закрывает соединение -- Закрывает соединение
socket:close() socket:close()

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

@ -7,7 +7,7 @@
- [расширения для string](#расширения-для-string) - [расширения для string](#расширения-для-string)
- [расширения для math](#расширения-для-math) - [расширения для math](#расширения-для-math)
- [расширения для bit](#расширения-для-bit) - [расширения для bit](#расширения-для-bit)
- [Дополнительные глобальные функции](#дополнительные-глобальные-функции) - [дополнительные глобальные функции](#дополнительные-глобальные-функции)
## Расширения для table ## Расширения для table
```lua ```lua
@ -62,6 +62,12 @@ table.sub(arr: table, start: number | nil, stop: number | nil) -> table
table.insert_unique(t: table, val: any) table.insert_unique(t: table, val: any)
table.insert_unique(t: table, pos: int, val: any) table.insert_unique(t: table, pos: int, val: any)
-- Возвращает таблицу, содержащую все ключи переданной таблицы, включая числовые.
table.keys(t: table) -> table
-- Добавляет в таблицу t все пары ключ-значение из таблицы extension, при этом если в extension присутствует ключ из t, то его значение будет перезаписано.
table.extend(t: table, extension: table) -> table
-- Конвертирует переданную таблицу в строку. -- Конвертирует переданную таблицу в строку.
table.tostring(t: table) -> string table.tostring(t: table) -> string
``` ```
@ -156,6 +162,14 @@ 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
-- Одномерный шум с значениями в диапазоне [-1..1].
math.noise(x: number, [опционально] octaves: int = 1) -> number
-- Двумерный шум с значениями в диапазоне [-1..1].
math.noise2d(x: number, y: number [опционально] octaves: int = 1) -> number
``` ```
## Расширения для bit ## Расширения для bit
@ -200,4 +214,4 @@ await(co: coroutine) -> result, error
-- Константа, в которой хранится PID текущего инстанса движка. -- Константа, в которой хранится PID текущего инстанса движка.
os.pid -> number os.pid -> number
``` ```

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

@ -18,7 +18,7 @@
Поток имеет три различных вида режима: Поток имеет три различных вида режима:
- Режим общего поведения (`general` / `mode`) - Режим общего поведения (`general` / `mode`)
- Режим сброса (`flush` / `flushMode`) - Режим сброса (`flush` / `flush_mode`)
- Двоичный режим (`binary`) - Двоичный режим (`binary`)
@ -27,34 +27,34 @@
Определяет, как поток обрабатывает чтение и запись, Определяет, как поток обрабатывает чтение и запись,
имеет три подрежима: имеет три подрежима:
| Режим | Описание | | Режим | Описание |
|---------------|--------| |---------------|-------------------------------------------------------------------------------------------------------------------------------------------------|
| `"default"` | Прямой режим. `read` может вернуть меньше байт, чем запрошено. `write` сразу отправляет данные в низкоуровневый дескриптор. Нет буферизации. | | `"default"` | Прямой режим. `read` может вернуть меньше байт, чем запрошено. `write` сразу отправляет данные в низкоуровневый дескриптор. Нет буферизации. |
| `"yield"` | Как `default`, но при нехватке данных в `read(n)` поток будет вызывать `coroutine.yield()`, пока не соберёт ровно `n` байт. Удобно для корутин. | | `"yield"` | Как `default`, но при нехватке данных в `read(n)` поток будет вызывать `coroutine.yield()`, пока не соберёт ровно `n` байт. Удобно для корутин. |
| `"buffered"` | Включает внутренние буферы чтения и записи. `read` берёт данные из буфера, `write` — складывает в буфер. При превышении `maxBufferSize` — ошибка `buffer overflow`. | | `"buffered"` | Включает внутренние буферы чтения и записи. `read` берёт данные из буфера , `write` — складывает в буфер. |
### flush ### flush
Работает только в режиме `"buffered"`, Работает только в режиме `"buffered"`,
имеет два подрежима: имеет два подрежима:
| Режим | Что делает `flush()` | | Режим | Что делает `flush()` |
|------------------|----------------------| |------------------------|-----------------------------------------------------------------------------------------------------|
| `"all"` (по умолчанию) | Сначала сбрасывает буфер записи → низкоуровневый `write`, затем вызывает `ioLib.flush(descriptor)` | | `"all"` (по умолчанию) | Сначала сбрасывает буфер записи → низкоуровневый `write`, затем вызывает `io_lib.flush(descriptor)` |
| `"buffer"` | Сбрасывает только буфер записи, без вызова системного `flush` | | `"buffer"` | Сбрасывает только буфер записи, без вызова системного `flush` |
### binary ### binary
**Независимый флаг** (включается через `set_binary_mode(true)` или при создании потока). **Независимый флаг** (включается через `set_binary_mode(true)` или при создании потока).
Определяет, в каком виде методы `read` и `write` принимают и возвращают данные: Определяет, в каком виде методы `read` и `write` принимают и возвращают данные:
| binary = true | binary = false (по умолчанию) | | binary = true | binary = false (по умолчанию) |
|-----------------------------------------|----------------------------------------| |------------------------------------------------------------|----------------------------------------|
| Данные — это **байты** (`Bytearray`, таблица чисел 0..255) | Данные — это **текстовые строки** | | Данные — это **байты** (`Bytearray`, таблица чисел 0..255) | Данные — это **текстовые строки** |
| `read(n)` → `Bytearray` или `table<number>`| `read()` → одна строка | | `read(n)` → `Bytearray` или `table<int>` | `read()` → одна строка |
| `read("i4 f")` → распаковка через `byteutil.unpack` | `read(n)` → n строк в таблице | | `read("i4 f")` → распаковка через `byteutil.unpack` | `read(n)` → n строк в таблице |
| `write(Bytearray)` → запись байтов | `write("hello")` → строка + `\n` | | `write(Bytearray)` → запись байтов | `write("hello")` → строка + `\n` |
| `write("i4", 42)` → `byteutil.pack` | `write({"a","b"})` → две строки с `\n`| | `write("i4", 42)` → `byteutil.pack` | `write({"a","b"})` → две строки с `\n` |
`read_line` / `write_line` — работают как в текстовом режиме `read_line` / `write_line` — работают как в текстовом режиме
@ -90,25 +90,25 @@ io_stream:set_flush_mode(string)
--[[ --[[
Читает данные из потока Читает данные из потока
В двоичном режиме: В двоичном режиме:
Если arg - number, то читает из потока arg байт и возвращает ввиде Bytearray или таблицы, если useTable = true Если arg - int, то читает из потока arg байт и возвращает ввиде Bytearray или таблицы, если use_table = true
Если arg - string, то функция интерпретирует arg как шаблон для byteutil. Прочитает кол-во байт, которое определено шаблоном, передаст их в byteutil.unpack и вернёт результат Если arg - string, то функция интерпретирует arg как шаблон для byteutil. Прочитает кол-во байт, которое определено шаблоном, передаст их в byteutil.unpack и вернёт результат
В текстовом режиме: В текстовом режиме:
Если arg - number, то читает нужное кол-во строк с окончанием CRLF/LF из arg и возвращает ввиде таблицы. Также, если trimEmptyLines = true, то удаляет пустые строки с начала и конца из итоговой таблицы Если arg - int, то читает нужное кол-во строк с окончанием CRLF/LF из arg и возвращает ввиде таблицы. Также, если trim_empty_lines = true, то удаляет пустые строки с начала и конца из итоговой таблицы
Если arg не определён, то читает одну строку с окончанием CRLF/LF и возвращает её. Если arg не определён, то читает одну строку с окончанием CRLF/LF и возвращает её.
--]] --]]
io_stream:read( io_stream:read(
[опционально] arg: number | string, [опционально] arg: int | string,
[опционально] useTable | trimEmptyLines: boolean [опционально] use_table = false | trim_empty_lines = true: boolean
) -> Bytearray | table<number> | string | table<string> | ... ) -> Bytearray | table<int> | string | table<string> | ...
--[[ --[[
Записывает данные в поток Записывает данные в поток
В двоичном режиме: В двоичном режиме:
Если arg - string, то функция интерпретирует arg как шаблон для byteutil, передаст его и ... в byteutil.pack и результат запишет в поток Если arg - string, то функция интерпретирует arg как шаблон для byteutil, передаст его и ... в byteutil.pack и результат запишет в поток
Если arg - Bytearray | table<number>, то записывает байты в поток Если arg - Bytearray | table<int>, то записывает байты в поток
В текстовом режиме: В текстовом режиме:
Если arg - string, то записывает строку в поток (вместе с окончанием LF) Если arg - string, то записывает строку в поток (вместе с окончанием LF)
@ -116,7 +116,7 @@ io_stream:read(
Если arg - table<string>, то записывает каждую строку из таблицы отдельно Если arg - table<string>, то записывает каждую строку из таблицы отдельно
--]] --]]
io_stream:write( io_stream:write(
arg: Bytearray | table<number> | string | table<string>, arg: Bytearray | table<int> | string | table<string>,
[опционально] ... [опционально] ...
) )
@ -128,53 +128,59 @@ io_stream:write_line(string)
--[[ --[[
В двоичном режиме: В двоичном режиме:
Читает все доступные байты из потока и возвращает ввиде Bytearray или table<number>, если useTable = true Читает все доступные байты из потока и возвращает ввиде Bytearray или table<int>, если use_table = true
В текстовом режиме: В текстовом режиме:
Читает все доступные строки из потока в table<string> если useTable = true, или в одну строку вместе с окончаниями, если нет Читает все доступные строки из потока в table<string> если use_table = true, или в одну строку вместе с окончаниями, если нет
--]] --]]
io_stream:read_fully( io_stream:read_fully(
[опционально] useTable: boolean [опционально] use_table: boolean
) -> Bytearray | table<number> | table<string> | string ) -> Bytearray | table<int> | table<string> | string
--[[ --[[
Устанавливает позицию в потоке Если length определён, то возвращает true, если length байт доступно к чтению. Иначе возвращает false.
Если не определён, то возвращает количество байт, которое можно прочитать.
В не буферизированном режиме потока может всегда возвращать 0 или false, если поток не поддерживает available.
--]]
io_stream:available(
[опционально] length: int
) -> int | boolean
--[[
Устанавливает позицию в потоке (всегда в байтах)
Режимы: Режимы:
b - Задаёт позицию относительно начало файла b - Задаёт позицию относительно начало файла
c - Задаёт позицию относительно текущей позиции c - Задаёт позицию относительно текущей позиции
e - Задаёт позицию относительно конца файла e - Задаёт позицию относительно конца файла
Может бросать ошибку, если поток не поддерживает seek.
--]] --]]
io_stream:seek( io_stream:seek(
mode: string mode: string
offset: number offset: int
) )
-- Возвращает текущую позицию в потоке от начала.
-- Может бросать ошибку, если поток не поддерживает tell.
io_stream:tell() -> int
``` ```
## Методы Buffered-режима ## Методы Buffered-режима
```lua ```lua
--[[
Если length определён, то возвращает true, если length байт доступно к чтению. Иначе возвращает false
Если не определён, то возвращает количество байт, которое можно прочитать
--]]
io_stream:available(
[опционально] length: number
) -> number | boolean
-- Возвращает максимальный размер буферов -- Возвращает максимальный размер буферов
io_stream:get_max_buffer_size() -> number io_stream:get_max_buffer_size() -> int
-- Задаёт новый максимальный размер буферов -- Задаёт новый максимальный размер буферов
io_stream:set_max_buffer_size(max_size: number) io_stream:set_max_buffer_size(max_size: int)
``` ```
## Методы контроля состояния потока ## Методы контроля состояния потока
```lua ```lua
-- Возвращает true, если поток открыт на данный момент -- Возвращает true, если поток открыт на данный момент
io_stream:is_alive() -> bool io_stream:is_alive() -> bool
@ -185,16 +191,27 @@ io_stream:is_closed() -> bool
io_stream:close() io_stream:close()
-- Записывает все данные из write-буфера в поток в buffer/all flush-режимах -- Записывает все данные из write-буфера в поток в buffer/all flush-режимах
-- Вызывает ioLib.flush() в all flush-режиме -- Вызывает ioLib.flush() в all flush-режиме, или ничего не делает, если
-- ioLib не поддерживает flush.
io_stream:flush() io_stream:flush()
-- Создаёт новый поток из Bytearray.
-- Может использоваться одновременно как для чтения, так и для записи.
-- Результат записи будет записан в тот же Bytearray, что был передан в функцию.
io_stream.wrap_bytearray(
buffer: Bytearray,
-- по-умолчанию равен true, поскольку функция из вводных аргументов
-- будет использоваться преимущественно для работы с двоичными данными.
[опционально] binary_mode: boolean = true
) -> io_stream
-- Создаёт новый поток с переданным дескриптором и использующим переданную I/O библиотеку. (Более подробно в core:io_stream.lua) -- Создаёт новый поток с переданным дескриптором и использующим переданную I/O библиотеку. (Более подробно в core:io_stream.lua)
io_stream.new( io_stream.new(
descriptor: int, descriptor: int,
binaryMode: bool, binary_mode: boolean,
ioLib: table, io_lib: table,
[опционально] mode: string = "default", [опционально] mode: string = "default",
[опционально] flushMode: string = "all" [опционально] flush_mode: string = "all"
) -> io_stream ) -> io_stream
``` ```

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

@ -19,7 +19,7 @@ local function update_line(line, uptime)
end end
end end
events.on("core:chat", function(message) local core_chat_handler = events.on("core:chat", function(message)
while #lines >= max_lines do while #lines >= max_lines do
document[lines[1][1]]:destruct() document[lines[1][1]]:destruct()
table.remove(lines, 1) table.remove(lines, 1)
@ -36,7 +36,7 @@ end)
function on_open() function on_open()
if not initialized then if not initialized then
initialized = true initialized = true
document.root:setInterval(1/animation_fps * 1000, function () document.root:setInterval(1/animation_fps * 1000, function ()
local uptime = time.uptime() local uptime = time.uptime()
for _, line in ipairs(lines) do for _, line in ipairs(lines) do
@ -52,3 +52,7 @@ function on_open()
end) end)
end end
end end
function on_destroy()
events.remove("core:chat", core_chat_handler)
end

View file

@ -11,7 +11,7 @@
@World generator @World generator
</button> </button>
<button id='content_btn' onclick='save_state() menu.page="content"'/> <button id='content_btn' onclick='save_state() menu.page="content"'/>
<button onclick='create_world()' margin='1,20,1,1'> <button onclick='create_world()' margin='0,20,0,1'>
@Create World @Create World
</button> </button>
<button onclick='menu:back()'>@Back</button> <button onclick='menu:back()'>@Back</button>

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

@ -148,3 +148,29 @@ function error(message, level)
end end
__error(message, level) __error(message, level)
end end
local LoggerClass = {
__index = {
info = function (self, text)
debug.log(string.format("[%s] %s", self.name, text))
end,
warning = function (self, text)
debug.warning(string.format("[%s] %s", self.name, text))
end,
error = function (self, text)
debug.error(string.format("[%s] %s", self.name, text))
end
}
}
local _getinfo = debug.getinfo
function debug.Logger(name)
if not name then
local source = _getinfo(2, "S").source
name = file.remove_ext(source)
else
name = string.format("%s:%s", file.prefix(_getinfo(2, "S").source), name)
end
return setmetatable({name=name}, LoggerClass)
end

View file

@ -14,6 +14,7 @@ function events.on(event, func)
events.handlers[event] = {} events.handlers[event] = {}
end end
table.insert(events.handlers[event], func) table.insert(events.handlers[event], func)
return func
end end
function events.reset(event, func) function events.reset(event, func)
@ -24,6 +25,14 @@ function events.reset(event, func)
end end
end end
function events.remove(event, handler)
local handlers = events.handlers[event]
if not handlers then
return
end
table.remove_value(handlers, handler)
end
function events.remove_by_prefix(prefix) function events.remove_by_prefix(prefix)
for name, handlers in pairs(events.handlers) do for name, handlers in pairs(events.handlers) do
local actualname = name local actualname = name

View file

@ -23,6 +23,10 @@ function file.parent(path)
return dir return dir
end end
function file.remove_ext(path)
return string.format("%s/%s", file.parent(path), file.stem(path))
end
function file.path(path) function file.path(path)
local pos = path:find(':') local pos = path:find(':')
return path:sub(pos + 1) return path:sub(pos + 1)

View file

@ -35,3 +35,89 @@ 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
local noise_period = 3021
local noise_rands = {}
for i=1,noise_period do
table.insert(noise_rands, math.random() * 2.0 - 1.0)
end
local function smoothstep(t)
return t * t * (3 - 2 * t)
end
local function sample_noise(x)
x = x % noise_period
local left_cell = math.floor(x)
local right_cell = (left_cell + 1) % noise_period
local t = smoothstep(x - left_cell)
return noise_rands[left_cell + 1] * (1.0 - t)
+ noise_rands[right_cell + 1] * t
end
function math.noise(x, octaves)
x = math.abs(x)
octaves = octaves or 1
local value = 0.0
local mul = 1.0
for i=1,octaves do
value = value * (1.0 - mul) + sample_noise(x + 37 * i) * mul
x = x * 2.0
mul = mul * 0.5
end
return value
end
local function lerp(a, b, t)
return a + (b - a) * t
end
local function rand_2d(x, y)
return noise_rands[math.floor(x * 317 + y) % noise_period + 1]
end
local function sample_noise2d(x, y)
x = math.abs(x)
y = math.abs(y)
local x0 = math.floor(x)
local y0 = math.floor(y)
local x1 = x0 + 1
local y1 = y0 + 1
local tx = x - x0
local ty = y - y0
local sx = smoothstep(tx)
local sy = smoothstep(ty)
local c00 = rand_2d(x0, y0)
local c10 = rand_2d(x1, y0)
local c01 = rand_2d(x0, y1)
local c11 = rand_2d(x1, y1)
local bottom = lerp(c00, c10, sx)
local top = lerp(c01, c11, sx)
return lerp(bottom, top, sy)
end
function math.noise2d(x, y, octaves)
octaves = octaves or 1
local value = 0.0
local mul = 1.0
for i=1,octaves do
value = value * (1.0 - mul)
+ sample_noise2d(x + 37 * i, y + 73 * i) * mul
x = x * 2.0
y = y * 2.0
mul = mul * 0.5
end
return value
end

View file

@ -59,11 +59,7 @@ end
function table.extend(t1, t2) function table.extend(t1, t2)
for i, v in pairs(t2) do for i, v in pairs(t2) do
if type(i) == "number" then t1[i] = v
t1[#t1 + 1] = v
else
t1[i] = v
end
end end
return t1 return t1
@ -201,3 +197,11 @@ function table.tostring(t)
end end
return s..']' return s..']'
end end
function table.keys(t)
local keys = {}
for key, _ in pairs(t) do
table.insert(keys, key)
end
return keys
end

View file

@ -77,15 +77,18 @@ function Element.new(docname, name)
end end
-- the engine automatically creates an instance for every ui document (layout) -- the engine automatically creates an instance for every ui document (layout)
local Document = {} local Document = {
function Document.new(docname) __index=function(self, k)
return setmetatable({name=docname}, { if type(k) ~= "string" then
__index=function(self, k) error("element id is not a string")
local elem = Element.new(self.name, k)
rawset(self, k, elem)
return elem
end end
}) local elem = Element.new(self.name, k)
rawset(self, k, elem)
return elem
end
}
function Document.new(docname)
return setmetatable({name=docname}, Document)
end end
local RadioGroup = {} local RadioGroup = {}

View file

@ -0,0 +1,131 @@
local io_stream = require "core:io_stream"
local lib = { }
local buffers = { }
local positions = { }
local next_descriptor = 0
local function open_descriptor(buffer)
next_descriptor = next_descriptor + 1
buffers[next_descriptor] = buffer
positions[next_descriptor] = 1
return next_descriptor
end
local function require_descriptor(descriptor)
if not buffers[descriptor] then
error("unknown descriptor")
end
end
function lib.read(descriptor, length)
require_descriptor(descriptor)
local buf = buffers[descriptor]
local buf_length = #buf
local pos = positions[descriptor]
local to_read = math.min(buf_length - pos + 1, length)
if to_read <= 0 then
return Bytearray()
end
local segment = buf:slice(pos, to_read)
positions[descriptor] = pos + to_read
return segment
end
function lib.write(descriptor, data)
require_descriptor(descriptor)
local buf = buffers[descriptor]
local pos = positions[descriptor]
local buf_length = #buf
local data_length = #data
local end_pos = pos + data_length - 1
-- size ensuring
if end_pos > buf_length then
for i = buf_length + 1, end_pos do
buf[i] = 0
end
end
for i = 1, data_length do
buf[i + pos - 1] = data[i]
end
positions[descriptor] = pos + data_length
end
function lib.seek(descriptor, mode, offset)
require_descriptor(descriptor)
local buf = buffers[descriptor]
local buf_length = #buf
local base
if mode == 'b' then
base = 1
elseif mode == 'c' then
base = positions[descriptor]
elseif mode == 'e' then
base = buf_length + 1
else error('invalid seek mode') end
local new_pos = base + offset
if new_pos < 1 then
error('failed to seek stream')
end
positions[descriptor] = new_pos
end
function lib.tell(descriptor)
require_descriptor(descriptor)
return positions[descriptor]
end
function lib.available(descriptor)
require_descriptor(descriptor)
local buf = buffers[descriptor]
local pos = positions[descriptor]
return math.max(#buf - pos + 1, 0)
end
function lib.is_alive(descriptor)
return buffers[descriptor] ~= nil
end
function lib.close(descriptor)
require_descriptor(descriptor)
buffers[descriptor] = nil
positions[descriptor] = nil
end
return function(buffer, binary_mode)
if binary_mode == nil then
binary_mode = true
end
return io_stream.new(
open_descriptor(buffer),
binary_mode,
lib
)
end

View file

@ -4,15 +4,28 @@ local lib = {
read = file.__read_descriptor, read = file.__read_descriptor,
write = file.__write_descriptor, write = file.__write_descriptor,
seek = file.__seek_descriptor, seek = file.__seek_descriptor,
tell = file.__tell_descriptor,
flush = file.__flush_descriptor, flush = file.__flush_descriptor,
available = file.__available_descriptor,
is_alive = file.__has_descriptor, is_alive = file.__has_descriptor,
close = file.__close_descriptor close = file.__close_descriptor
} }
local __open_descriptor = file.__open_descriptor
file.__open_descriptor = nil
file.__read_descriptor = nil
file.__write_descriptor = nil
file.__seek_descriptor = nil
file.__tell_descriptor = nil
file.__flush_descriptor = nil
file.__available_descriptor = nil
file.__has_descriptor = nil
file.__close_descriptor = nil
return function(path, mode) return function(path, mode)
return io_stream.new( return io_stream.new(
file.__open_descriptor(path, mode), __open_descriptor(path, mode),
mode:find('b') ~= nil, mode:find('b') ~= nil,
lib lib
) )
end end

View file

@ -9,6 +9,7 @@ int close(int fd);
ssize_t read(int fd, void *buf, size_t count); ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count); ssize_t write(int fd, const void *buf, size_t count);
int fcntl(int fd, int cmd, ...); int fcntl(int fd, int cmd, ...);
int ioctl(int fd, unsigned long request, ...);
const char *strerror(int errnum); const char *strerror(int errnum);
]] ]]
@ -20,6 +21,7 @@ local O_WRONLY = 0x1
local O_RDWR = 0x2 local O_RDWR = 0x2
local O_NONBLOCK = 0x800 local O_NONBLOCK = 0x800
local F_GETFL = 3 local F_GETFL = 3
local FIONREAD = 0x541B
local function getError() local function getError()
local err = FFI.errno() local err = FFI.errno()
@ -58,12 +60,16 @@ function lib.write(fd, bytearray)
end end
end end
function lib.seek(fd, mode, offset) function lib.available(fd)
error("cannot seek the named pipe") if fd == nil or fd < 0 then return 0 end
end
function lib.flush(fd) local bytes_ready = FFI.new("int[1]", 0)
-- no flush on unix
if C.ioctl(fd, FIONREAD, bytes_ready) == -1 then
return 0
end
return tonumber(bytes_ready[0])
end end
function lib.is_alive(fd) function lib.is_alive(fd)

View file

@ -97,14 +97,26 @@ function lib.write(handle, bytearray)
end end
end end
function lib.seek(handle, mode, offset)
error("cannot seek the named pipe")
end
function lib.flush(handle) function lib.flush(handle)
C.FlushFileBuffers(handle) C.FlushFileBuffers(handle)
end end
function lib.available(handle)
if handle == nil or handle == INVALID_HANDLE_VALUE then
return 0
end
local bytes_available = FFI.new("DWORD[1]")
local success = C.PeekNamedPipe(handle, nil, 0, nil, bytes_available, nil)
if success == 0 then
return 0
end
return tonumber(bytes_available[0])
end
function lib.is_alive(handle) function lib.is_alive(handle)
if handle == nil or handle == INVALID_HANDLE_VALUE then if handle == nil or handle == INVALID_HANDLE_VALUE then
return false return false

View file

@ -0,0 +1,64 @@
local io_stream = require "core:io_stream"
local lib = { }
local sockets = { }
local next_descriptor = 0
local function open_descriptor(socket)
next_descriptor = next_descriptor + 1
sockets[next_descriptor] = socket
return next_descriptor
end
local function require_descriptor(descriptor)
if not sockets[descriptor] then
error("unknown descriptor")
end
end
function lib.read(descriptor, length)
require_descriptor(descriptor)
return sockets[descriptor]:recv(length)
end
function lib.write(descriptor, data)
require_descriptor(descriptor)
sockets[descriptor]:send(data)
end
function lib.available(descriptor)
require_descriptor(descriptor)
return sockets[descriptor]:available()
end
function lib.is_alive(descriptor)
local socket = sockets[descriptor]
return socket ~= nil and socket:is_alive()
end
function lib.close(descriptor)
require_descriptor(descriptor)
sockets[descriptor]:close()
sockets[descriptor] = nil
end
return function(socket, binary_mode)
if binary_mode == nil then
binary_mode = true
end
return io_stream.new(
open_descriptor(socket),
binary_mode,
lib
)
end

View file

@ -0,0 +1,139 @@
local __app = __vc_app
local function get_node_center(node)
local pos = node.wpos
local size = node.size
return pos[1] + size[1] / 2, pos[2] + size[2] / 2
end
local function types_check_enum(value, enum)
if value == nil then
return true, nil
end
if type(value) ~= "string" then
return false, string.format("string expected [%s]", table.concat(table.keys(enum), "|"))
end
return true, nil
end
local function types_check_number(value)
if value == nil then
return true, nil
end
local value_type = type(value)
if value_type ~= "number" then
return false, string.format("number expected, got %s", value_type)
end
end
local function check_option(t, key, func, ...)
local value = t[key]
if value == nil then
return nil
end
local success, message = func(value, ...)
if not success then
error(string.format("invalid option '%s': %s", key, message))
end
end
local buttons_enum = {
left = 0,
right = 1,
middle = 2,
}
local no_options = {}
local _tick = __app.tick
local _set_button_pressed = test.set_button_pressed
local _set_key_pressed = test.set_key_pressed
local _enter_text = test.enter_text
test.set_button_pressed = nil
test.set_key_pressed = nil
test.enter_text = nil
function test.click(node, options)
options = options or no_options
check_option(options, 'button', types_check_enum, buttons_enum)
check_option(options, 'timeout', types_check_number)
local button = buttons_enum[options.button or 'left']
local timeout = options.timeout or 1000
local tm = time.precise_time()
while (time.precise_time() - tm) * 1000 < timeout do
if node.exists then
break
end
_tick()
end
if not node.exists then
error(string.format("timeout %s ms exceeded: element %s does not exists",
math.floor(timeout), string.escape(rawget(node, 'name'))))
end
local center_x, center_y = get_node_center(node)
_set_button_pressed(button, center_x, center_y, true)
_tick()
_set_button_pressed(button, center_x, center_y, false)
_tick()
end
-- TODO: replace with resolving in C++ side
function test.find_by_attr(node, attr, value)
if getmetatable(node) == Document then
return test.find_by_attr(node.root, attr, value)
end
if node[attr] == value then
return node
end
local i = 1
while true do
local subnode = node[i]
if subnode == nil then
break
end
local found = test.find_by_attr(subnode, attr, value)
if found then
return found
end
i = i + 1
end
end
function test.find_by_text(node, text)
return test.find_by_attr(node, "text", text)
end
function test.find_by_attr_presence(node, attr)
if node[attr] then
return node
end
local i = 1
while true do
local subnode = node[i]
if subnode == nil then
break
end
local found = test.find_by_attr_presence(subnode, attr)
if found then
return found
end
i = i + 1
end
end
function test.fill(node, text, options)
test.click(node, options)
__app.tick()
_enter_text(text)
test.press("enter")
end
function test.press(key)
_set_key_pressed(key, true)
__app.tick()
_set_key_pressed(key, false)
__app.tick()
end

View file

@ -25,13 +25,13 @@ local ALL_FLUSH_MODES = {
local CR = string.byte('\r') local CR = string.byte('\r')
local LF = string.byte('\n') local LF = string.byte('\n')
local function readFully(result, readFunc) local function read_fully(result, read_func)
local isTable = type(result) == "table" local isTable = type(result) == "table"
local buf local buf
repeat repeat
buf = readFunc(MAX_BUFFER_SIZE) buf = read_func(MAX_BUFFER_SIZE)
if isTable then if isTable then
for i = 1, #buf do for i = 1, #buf do
@ -42,41 +42,46 @@ local function readFully(result, readFunc)
end end
--[[ --[[
descriptor - descriptor of stream for provided I/O library descriptor - descriptor of stream for provided I/O library
binaryMode - if enabled, most methods will expect bytes instead of strings binaryMode - if enabled, most methods will expect bytes instead of strings
ioLib - I/O library. Should include the following functions: ioLib - I/O library. Should include the following functions:
read(descriptor: int, length: int) -> Bytearray read(descriptor: int, length: int) -> Bytearray
May return bytearray with a smaller size if bytes have not arrived yet or have run out May return bytearray with a smaller size if bytes have not arrived yet or have run out
May throw error if descriptor isn't readable
write(descriptor: int, data: Bytearray) write(descriptor: int, data: Bytearray)
flush(descriptor: int) May throw error if descriptor isn't writeable
[optional] seek(descriptor: int, mode: string, offset: int)
Mode may be 'b' (relative begin), 'c' (relative current), 'e' (relative end + 1)
[optional] tell(descriptor: int) -> int
[optional] flush(descriptor: int)
[optional] available(descriptor: int) -> int
is_alive(descriptor: int) -> bool is_alive(descriptor: int) -> bool
close(descriptor: int) close(descriptor: int)
--]] --]]
function io_stream.new(descriptor, binaryMode, ioLib, mode, flushMode) function io_stream.new(descriptor, binary_mode, io_lib, mode, flush_mode)
mode = mode or DEFAULT_MODE mode = mode or DEFAULT_MODE
flushMode = flushMode or FLUSH_MODE_ALL flush_mode = flush_mode or FLUSH_MODE_ALL
local self = setmetatable({}, io_stream) local self = setmetatable({}, io_stream)
self.descriptor = descriptor self.descriptor = descriptor
self.binaryMode = binaryMode self.binary_mode = binary_mode
self.maxBufferSize = MAX_BUFFER_SIZE self.max_buffer_size = MAX_BUFFER_SIZE
self.ioLib = ioLib self.io_lib = io_lib
self:set_mode(mode) self:set_mode(mode)
self:set_flush_mode(flushMode) self:set_flush_mode(flush_mode)
return self return self
end end
function io_stream:is_binary_mode() function io_stream:is_binary_mode()
return self.binaryMode return self.binary_mode
end end
function io_stream:set_binary_mode(binaryMode) function io_stream:set_binary_mode(binary_mode)
self.binaryMode = binaryMode ~= nil self.binary_mode = binary_mode ~= nil
end end
function io_stream:get_mode() function io_stream:get_mode()
@ -88,124 +93,141 @@ function io_stream:set_mode(mode)
error("invalid stream mode: "..mode) error("invalid stream mode: "..mode)
end end
if self.mode == BUFFERED_MODE then if self.write_buffer then
self.writeBuffer:clear() self.write_buffer:clear()
self.readBuffer:clear() self.write_buffer = nil
end end
if mode == BUFFERED_MODE and not self.writeBuffer then if self.read_buffer then
self.writeBuffer = Bytearray() self.read_buffer:clear()
self.readBuffer = Bytearray() self.read_buffer = nil
end
if mode == BUFFERED_MODE then
self.write_buffer = Bytearray(self.max_buffer_size)
self.read_buffer = Bytearray(self.max_buffer_size)
end end
self.mode = mode self.mode = mode
end end
function io_stream:get_flush_mode() function io_stream:get_flush_mode()
return self.flushMode return self.flush_mode
end end
function io_stream:set_flush_mode(flushMode) function io_stream:set_flush_mode(flush_mode)
if not table.has(ALL_FLUSH_MODES, flushMode) then if not table.has(ALL_FLUSH_MODES, flush_mode) then
error("invalid flush mode: "..flushMode) error("invalid flush mode: " .. flush_mode)
end end
self.flushMode = flushMode self.flush_mode = flush_mode
end end
function io_stream:get_max_buffer_size() function io_stream:get_max_buffer_size()
return self.maxBufferSize return self.max_buffer_size
end end
function io_stream:set_max_buffer_size(maxBufferSize) function io_stream:set_max_buffer_size(max_buffer_size)
self.maxBufferSize = maxBufferSize self.max_buffer_size = max_buffer_size
self.write_buffer = Bytearray(self.max_buffer_size)
self.read_buffer = Bytearray(self.max_buffer_size)
end end
function io_stream:available(length) function io_stream:available(length)
local available = self.io_lib.available and self.io_lib.available(self.descriptor) or 0
if self.mode == BUFFERED_MODE then if self.mode == BUFFERED_MODE then
self:__update_read_buffer() available = available + #self.read_buffer
end
if not length then if not length then
return #self.readBuffer return available
else else
return #self.readBuffer >= length return available >= length
end
end end
end end
function io_stream:__update_read_buffer() function io_stream:__read(length, from_read_fully)
local readed = Bytearray()
readFully(readed, function(length) return self.ioLib.read(self.descriptor, length) end)
self.readBuffer:append(readed)
if #self.readBuffer > self.maxBufferSize then
error "buffer overflow"
end
end
function io_stream:__read(length)
if self.mode == YIELD_MODE then if self.mode == YIELD_MODE then
if from_read_fully then
return self.io_lib.read(self.descriptor, length)
end
local buffer = Bytearray() local buffer = Bytearray()
while #buffer < length do while #buffer < length do
buffer:append(self.ioLib.read(self.descriptor, length - #buffer)) buffer:append(self.io_lib.read(self.descriptor, length - #buffer))
if #buffer < length then coroutine.yield() end if #buffer < length then coroutine.yield() end
end end
return buffer return buffer
elseif self.mode == BUFFERED_MODE then elseif self.mode == BUFFERED_MODE then
self:__update_read_buffer() local buf_len = #self.read_buffer
if #self.readBuffer < length then if buf_len < length then
error "buffer underflow" self.read_buffer:append(
self.io_lib.read(self.descriptor, self.max_buffer_size - buf_len)
)
end end
local copy buf_len = #self.read_buffer
if #self.readBuffer == length then length = math.min(buf_len, length)
copy = Bytearray()
copy:append(self.readBuffer)
self.readBuffer:clear() local copy = self.read_buffer:slice(1, length)
if buf_len == length then
self.read_buffer:clear()
else else
copy = Bytearray() self.read_buffer:remove(1, length)
for i = 1, length do
copy[i] = self.readBuffer[i]
end
self.readBuffer:remove(1, length)
end end
return copy return copy
elseif self.mode == DEFAULT_MODE then elseif self.mode == DEFAULT_MODE then
return self.ioLib.read(self.descriptor, length) return self.io_lib.read(self.descriptor, length)
end end
end end
function io_stream:__write(data) function io_stream:__write(data)
if self.mode == BUFFERED_MODE then if self.mode == BUFFERED_MODE then
self.writeBuffer:append(data) local data_length = #data
if #self.writeBuffer > self.maxBufferSize then if #self.write_buffer + data_length > self.max_buffer_size then
error "buffer overflow" self:flush()
end end
if data_length > self.max_buffer_size then
local to_write = math.floor(data_length / self.max_buffer_size) * self.max_buffer_size
local to_save = data_length - to_write
self.io_lib.write(self.descriptor, data:slice(1, to_write))
self:flush()
self.write_buffer = data:slice(to_write + 1, to_save)
else self.write_buffer:append(data) end
elseif self.mode == DEFAULT_MODE or self.mode == YIELD_MODE then elseif self.mode == DEFAULT_MODE or self.mode == YIELD_MODE then
return self.ioLib.write(self.descriptor, data) return self.io_lib.write(self.descriptor, data)
end end
end end
function io_stream:read_fully(useTable) function io_stream:read_fully(use_table)
if self.binaryMode then if self.binary_mode then
local result = useTable and Bytearray() or { } local result = use_table and { } or Bytearray()
readFully(result, function() return self:__read(self.maxBufferSize) end) local avail = self:available()
if avail == 0 then
avail = self.max_buffer_size
end
read_fully(result, function() return self:__read(avail, true) end)
return result
else else
if useTable then if use_table then
local lines = { } local lines = { }
local line local line
@ -220,7 +242,7 @@ function io_stream:read_fully(useTable)
else else
local result = Bytearray() local result = Bytearray()
readFully(result, function() return self:__read(self.maxBufferSize) end) read_fully(result, function() return self:__read(self.max_buffer_size) end)
return utf8.tostring(result) return utf8.tostring(result)
end end
@ -262,57 +284,61 @@ function io_stream:write_line(str)
self:__write(utf8.tobytes(str .. "\n")) self:__write(utf8.tobytes(str .. "\n"))
end end
function io_stream:read(arg, useTable) function io_stream:read(arg, use_table)
local argType = type(arg) local arg_type = type(arg)
if self.binaryMode then if self.binary_mode then
local byteArr local byte_arr
if argType == "number" then if arg_type == "number" then
-- using 'arg' as length -- using 'arg' as length
byteArr = self:__read(arg) byte_arr = self:__read(arg)
if useTable == true then if use_table == true then
local t = { } local t = { }
for i = 1, #byteArr do for i = 1, #byte_arr do
t[i] = byteArr[i] t[i] = byte_arr[i]
end end
return t return t
else else
return byteArr return byte_arr
end end
elseif argType == "string" then elseif arg_type == "string" then
return byteutil.unpack( return byteutil.unpack(
arg, arg,
self:__read(byteutil.get_size(arg)) self:__read(byteutil.get_size(arg))
) )
elseif argType == nil then elseif arg_type == "nil" then
error( error(
"in binary mode the first argument must be a string data format".. "in binary mode the first argument must be a string data format"..
" for the library \"byteutil\" or the number of bytes to read" " for the library \"byteutil\" or the number of bytes to read"
) )
else else
error("unknown argument type: "..argType) error("unknown argument type: "..arg_type)
end end
else else
if not arg then if not arg then
return self:read_line() return self:read_line()
else else
local linesCount = arg local lines_count = arg
local trimLastEmptyLines = useTable or true local trim_last_empty_lines = use_table
if linesCount < 0 then error "count of lines to read must be positive" end if use_table == nil then
trim_last_empty_lines = true
end
if lines_count < 0 then error "count of lines to read must be positive" end
local result = { } local result = { }
for i = 1, linesCount do for i = 1, lines_count do
result[i] = self:read_line() result[i] = self:read_line()
end end
if trimLastEmptyLines then if trim_last_empty_lines then
local i = #result local i = #result
while i >= 0 do while i >= 0 do
@ -340,41 +366,53 @@ function io_stream:read(arg, useTable)
end end
function io_stream:write(arg, ...) function io_stream:write(arg, ...)
local argType = type(arg) local arg_type = type(arg)
if self.binaryMode then if self.binary_mode then
local byteArr local byte_arr
if argType ~= "string" then if arg_type ~= "string" then
-- using arg as bytes table/bytearray -- using arg as bytes table/bytearray
if argType == "table" then if arg_type == "table" then
byteArr = Bytearray(arg) byte_arr = Bytearray(arg)
else else
byteArr = arg byte_arr = arg
end end
else else
byteArr = byteutil.pack(arg, ...) byte_arr = byteutil.pack(arg, ...)
end end
self:__write(byteArr) self:__write(byte_arr)
else else
if argType == "string" then if arg_type == "string" then
self:write_line(arg) self:write_line(arg)
elseif argType == "table" then elseif arg_type == "table" then
for i = 1, #arg do for i = 1, #arg do
self:write_line(arg[i]) self:write_line(arg[i])
end end
else error("unknown argument type: "..argType) end else error("unknown argument type: "..arg_type) end
end end
end end
function io_stream:seek(mode, offset) function io_stream:seek(mode, offset)
self.ioLib.seek(self.descriptor, mode, offset) if not self.io_lib.seek then
error("cannot seek this stream")
end
self.io_lib.seek(self.descriptor, mode, offset)
end
function io_stream:tell()
if not self.io_lib.tell then
error("cannot tell this stream")
end
return self.io_lib.tell(self.descriptor)
end end
function io_stream:is_alive() function io_stream:is_alive()
return self.ioLib.is_alive(self.descriptor) return self.io_lib.is_alive(self.descriptor)
end end
function io_stream:is_closed() function io_stream:is_closed()
@ -383,20 +421,24 @@ end
function io_stream:close() function io_stream:close()
if self.mode == BUFFERED_MODE then if self.mode == BUFFERED_MODE then
self.readBuffer:clear() self.read_buffer:clear()
self.writeBuffer:clear() self.write_buffer:clear()
end end
return self.ioLib.close(self.descriptor) return self.io_lib.close(self.descriptor)
end end
function io_stream:flush() function io_stream:flush()
if self.mode == BUFFERED_MODE and #self.writeBuffer > 0 then if self.mode == BUFFERED_MODE and #self.write_buffer > 0 then
self.ioLib.write(self.descriptor, self.writeBuffer) self.io_lib.write(self.descriptor, self.write_buffer)
self.writeBuffer:clear() self.write_buffer:clear()
end end
if self.flushMode ~= FLUSH_MODE_ONLY_BUFFER then self.ioLib.flush(self.descriptor) end if self.flush_mode ~= FLUSH_MODE_ONLY_BUFFER then
if self.io_lib.flush then
self.io_lib.flush(self.descriptor)
elseif self:is_closed() then error("stream is closed") end
end
end end
return io_stream return io_stream

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

@ -49,6 +49,7 @@ local Socket = {__index={
end end
return self:recv(length, usetable) return self:recv(length, usetable)
end, end,
as_stream=network.__as_stream,
close=function(self) return network.__close(self.id) end, close=function(self) return network.__close(self.id) end,
available=function(self) return network.__available(self.id) or 0 end, available=function(self) return network.__available(self.id) or 0 end,
is_alive=function(self) return network.__is_alive(self.id) end, is_alive=function(self) return network.__is_alive(self.id) end,

View file

@ -142,14 +142,16 @@ 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({dir[3], dir[1]})) % 360 local prev_angle = (vec2.angle({dir[3], 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

@ -127,6 +127,10 @@ require "core:internal/extensions/inventory"
asserts = require "core:internal/asserts" asserts = require "core:internal/asserts"
events = require "core:internal/events" events = require "core:internal/events"
if test then
require "core:internal/test"
end
function pack.unload(prefix) function pack.unload(prefix)
events.remove_by_prefix(prefix) events.remove_by_prefix(prefix)
end end
@ -312,6 +316,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"
@ -332,6 +338,10 @@ else
os.pid = ffi.C.getpid() os.pid = ffi.C.getpid()
end end
require("core:io_stream").wrap_bytearray = require "core:internal/stream_providers/bytearray"
network.__as_stream = require "core:internal/stream_providers/socket"
math.randomseed(time.uptime() * 1536227939) math.randomseed(time.uptime() * 1536227939)
rules = require "core:internal/rules" rules = require "core:internal/rules"

View file

@ -37,6 +37,14 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
"$ENV{VCPKG_ROOT}/packages/luajit_${VCPKG_TARGET_TRIPLET}/include/" "$ENV{VCPKG_ROOT}/packages/luajit_${VCPKG_TARGET_TRIPLET}/include/"
) )
else() else()
set(LUAJIT_PACKAGE_DIR "$ENV{VCPKG_ROOT}/packages/luajit_${VCPKG_TARGET_TRIPLET}")
if(EXISTS "$ENV{VCPKG_ROOT}/packages/luajit_${VCPKG_TARGET_TRIPLET}/include/luajit-2.1")
set(LUAJIT_INCLUDE_DIR "$ENV{VCPKG_ROOT}/packages/luajit_${VCPKG_TARGET_TRIPLET}/include/luajit-2.1")
else()
set(LUAJIT_INCLUDE_DIR "$ENV{VCPKG_ROOT}/packages/luajit_${VCPKG_TARGET_TRIPLET}/include/luajit")
endif()
add_library(luajit SHARED IMPORTED) add_library(luajit SHARED IMPORTED)
set_target_properties( set_target_properties(
luajit luajit
@ -45,8 +53,7 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
"$ENV{VCPKG_ROOT}/packages/luajit_${VCPKG_TARGET_TRIPLET}/bin/lua51.dll" "$ENV{VCPKG_ROOT}/packages/luajit_${VCPKG_TARGET_TRIPLET}/bin/lua51.dll"
IMPORTED_IMPLIB IMPORTED_IMPLIB
"$ENV{VCPKG_ROOT}/packages/luajit_${VCPKG_TARGET_TRIPLET}/lib/lua51.lib" "$ENV{VCPKG_ROOT}/packages/luajit_${VCPKG_TARGET_TRIPLET}/lib/lua51.lib"
INTERFACE_INCLUDE_DIRECTORIES INTERFACE_INCLUDE_DIRECTORIES "${LUAJIT_INCLUDE_DIR}"
"$ENV{VCPKG_ROOT}/packages/luajit_${VCPKG_TARGET_TRIPLET}/include/luajit"
) )
endif() endif()

View file

@ -173,6 +173,12 @@ void audio::initialize(
logger.info() << "initializing NoAudio backend"; logger.info() << "initializing NoAudio backend";
backend = NoAudio::create().release(); backend = NoAudio::create().release();
} }
if (settings.inputDevice.get() == "auto") {
auto inputDevices = audio::get_input_devices_names();
if (!inputDevices.empty()) {
settings.inputDevice.set(inputDevices.at(0));
}
}
struct { struct {
std::string name; std::string name;
NumberSetting* setting; NumberSetting* setting;
@ -210,7 +216,9 @@ void audio::initialize(
::input_device = backend->openInputDevice( ::input_device = backend->openInputDevice(
settings.inputDevice.get(), 44100, 1, 16 settings.inputDevice.get(), 44100, 1, 16
); );
::input_device->startCapture(); if (::input_device != nullptr) {
::input_device->startCapture();
}
} else { } else {
if (::input_device) { if (::input_device) {
::input_device->stopCapture(); ::input_device->stopCapture();

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

@ -57,3 +57,7 @@ std::string parsing_error::errorLog() const {
ss << "^"; ss << "^";
return ss.str(); return ss.str();
} }
std::runtime_error parsing_error::toRuntimeError() const {
return std::runtime_error("parsing error: " + errorLog());
}

View file

@ -73,4 +73,6 @@ public:
uint linestart uint linestart
); );
std::string errorLog() const; std::string errorLog() const;
std::runtime_error toRuntimeError() const;
}; };

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

@ -30,6 +30,7 @@ namespace {
public: public:
FTFontFile(FT_Face face, util::Buffer<ubyte> buffer) FTFontFile(FT_Face face, util::Buffer<ubyte> buffer)
: face(std::move(face)), buffer(std::move(buffer)) { : face(std::move(face)), buffer(std::move(buffer)) {
monospace = this->face->face_flags & FT_FACE_FLAG_FIXED_WIDTH;
} }
~FTFontFile() { ~FTFontFile() {

View file

@ -19,6 +19,12 @@ namespace vector_fonts {
virtual std::unique_ptr<Texture> renderPage( virtual std::unique_ptr<Texture> renderPage(
int pageid, std::vector<Glyph>& glyphs, int size int pageid, std::vector<Glyph>& glyphs, int size
) = 0; ) = 0;
bool isMonospace() const {
return monospace;
}
protected:
bool monospace = true;
}; };
void initialize(); void initialize();

View file

@ -385,10 +385,22 @@ public:
} }
if (c == '(') { if (c == '(') {
nextChar(); nextChar();
// TODO: replace with array parsing after moving to dv::value's int depth = 1;
std::string value = std::string(readUntil(')')); size_t start = pos;
while (hasNext()) {
char c = nextChar();
if (c == '(') {
depth++;
} else if (c == ')') {
depth--;
if (depth == 0) {
goBack(1);
break;
}
}
}
expect(')'); expect(')');
return value; return std::string(source.substr(start, pos - start - 1));
} }
return std::string(readUntilWhitespace()); return std::string(readUntilWhitespace());
} }
@ -528,3 +540,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

@ -188,6 +188,7 @@ public:
ContentUnitDefs<GeneratorDef> generators; ContentUnitDefs<GeneratorDef> generators;
std::unique_ptr<DrawGroups> const drawGroups; std::unique_ptr<DrawGroups> const drawGroups;
ResourceIndicesSet resourceIndices {}; ResourceIndicesSet resourceIndices {};
std::set<std::string> components {};
Content( Content(
std::unique_ptr<ContentIndices> indices, std::unique_ptr<ContentIndices> indices,

View file

@ -178,7 +178,16 @@ void ContentUnitLoader<DefT>::loadUnit(
) { ) {
auto folder = pack.folder; auto folder = pack.folder;
auto configFile = folder / (defsDir + "/" + name + ".json"); auto configFile = folder / (defsDir + "/" + name + ".json");
if (io::exists(configFile)) loadUnit(def, full, configFile); if (io::exists(configFile)) {
try {
loadUnit(def, full, configFile);
} catch (const std::runtime_error& err) {
throw std::runtime_error(
"file " + util::quote(configFile.string()) + ": " +
std::string(err.what())
);
}
}
} }
void ContentLoader::loadBlockMaterial( void ContentLoader::loadBlockMaterial(
@ -497,6 +506,7 @@ void ContentLoader::loadScripts(Content& content) {
scripting::on_scripts_loading(); scripting::on_scripts_loading();
load_scripts(content, content.blocks); load_scripts(content, content.blocks);
load_scripts(content, content.items); load_scripts(content, content.items);
auto& tmpContent = content;
for (const auto& [packid, runtime] : content.getPacks()) { for (const auto& [packid, runtime] : content.getPacks()) {
auto env = runtime->getEnvironment(); auto env = runtime->getEnvironment();
@ -507,7 +517,7 @@ void ContentLoader::loadScripts(Content& content) {
// Load entity components // Load entity components
io::path componentsDir = folder / "scripts/components"; io::path componentsDir = folder / "scripts/components";
foreach_file(componentsDir, [&pack, env](const io::path& file) { foreach_file(componentsDir, [&pack, env, &tmpContent](const io::path& file) {
auto name = pack.id + ":" + file.stem(); auto name = pack.id + ":" + file.stem();
scripting::load_entity_component( scripting::load_entity_component(
env, env,
@ -515,9 +525,21 @@ void ContentLoader::loadScripts(Content& content) {
file, file,
pack.id + ":scripts/components/" + file.name() pack.id + ":scripts/components/" + file.name()
); );
tmpContent.components.insert(name);
}); });
} }
for (const auto& [eid, def] : content.entities.getDefs()) {
for (const auto& instance : def->components) {
if (content.components.find(instance.component) == content.components.end()) {
throw std::runtime_error(
"component " + instance.component +
" is not available (required by entity " + eid + ")"
);
}
}
}
scripting::on_content_loaded(); scripting::on_content_loaded();
} }

View file

@ -53,6 +53,9 @@ static void load_variant(
} }
} else if (root.has("texture-faces")) { } else if (root.has("texture-faces")) {
const auto& texarr = root["texture-faces"]; const auto& texarr = root["texture-faces"];
if (texarr.length() < 6) {
throw std::runtime_error("texture-faces requires 6 elements");
}
for (uint i = 0; i < 6; i++) { for (uint i = 0; i < 6; i++) {
variant.textureFaces[i] = texarr[i].asString(); variant.textureFaces[i] = texarr[i].asString();
} }

View file

@ -0,0 +1,63 @@
#include "AppScriptsControl.hpp"
#include "debug/Logger.hpp"
#include "engine/CoreParameters.hpp"
#include "io/io.hpp"
#include "Project.hpp"
#include "logic/scripting/scripting.hpp"
static debug::Logger logger("app-scripts");
AppScriptsControl::AppScriptsControl(
const CoreParameters& params, const Project& project
)
: project(project) {
io::path scriptFile =
params.scriptFile.empty()
? "project:start.lua"
: std::string("script:") + params.scriptFile.filename().u8string();
if (io::exists(scriptFile)) {
logger.info() << "starting script: " << params.scriptFile.u8string();
scriptCoroutine = scripting::start_app_script(scriptFile);
} else {
logger.warning() << "script does not exists: " << params.scriptFile.u8string();
}
if (!params.headless) {
loadProjectClientScript();
}
}
void AppScriptsControl::loadProjectClientScript() {
io::path scriptFile = "project:project_client.lua";
if (io::exists(scriptFile)) {
logger.info() << "starting project client script: " << scriptFile.string();
clientScript = scripting::load_client_project_script(scriptFile);
} else {
logger.warning() << "project client script does not exists: " << scriptFile.string();
}
}
void AppScriptsControl::onScreenChange(const std::string& name, bool show) {
if (clientScript) {
clientScript->onScreenChange(name, show);
}
}
void AppScriptsControl::tick() {
if (scriptCoroutine && scriptCoroutine->isActive()) {
scriptCoroutine->update();
}
}
void AppScriptsControl::terminate(std::string_view reason) {
if (scriptCoroutine->isActive()) {
scriptCoroutine->terminate();
logger.info() << "script has been terminated due to " << reason;
}
}
bool AppScriptsControl::isFinished() const {
return scriptCoroutine == nullptr || !scriptCoroutine->isActive();
}

View file

@ -0,0 +1,30 @@
#pragma once
#include "interfaces/Process.hpp"
#include <memory>
#include <string>
namespace scripting {
class IClientProjectScript;
}
struct CoreParameters;
struct Project;
class AppScriptsControl {
public:
AppScriptsControl(const CoreParameters& params, const Project& project);
void tick();
void loadProjectClientScript();
void terminate(std::string_view reason);
void onScreenChange(const std::string& name, bool show);
bool isFinished() const;
private:
const Project& project;
std::unique_ptr<scripting::IClientProjectScript> clientScript;
std::unique_ptr<Process> scriptCoroutine;
};

View file

@ -2,6 +2,7 @@
#include "data/dv_util.hpp" #include "data/dv_util.hpp"
#include "debug/Logger.hpp" #include "debug/Logger.hpp"
#include "engine/CoreParameters.hpp"
#include "io/io.hpp" #include "io/io.hpp"
#include "io/path.hpp" #include "io/path.hpp"
#include "logic/scripting/scripting.hpp" #include "logic/scripting/scripting.hpp"
@ -40,26 +41,6 @@ void Project::deserialize(const dv::value& src) {
} }
} }
void Project::loadProjectClientScript() {
io::path scriptFile = "project:project_client.lua";
if (io::exists(scriptFile)) {
logger.info() << "starting project client script";
clientScript = scripting::load_client_project_script(scriptFile);
} else {
logger.warning() << "project client script does not exists";
}
}
void Project::loadProjectStartScript() {
io::path scriptFile = "project:start.lua";
if (io::exists(scriptFile)) {
logger.info() << "starting project start script";
setupCoroutine = scripting::start_app_script(scriptFile);
} else {
logger.warning() << "project start script does not exists";
}
}
bool Permissions::has(const std::string& name) const { bool Permissions::has(const std::string& name) const {
return permissions.find(name) != permissions.end(); return permissions.find(name) != permissions.end();
} }

View file

@ -1,17 +1,12 @@
#pragma once #pragma once
#include "interfaces/Serializable.hpp"
#include <set> #include <set>
#include <string> #include <string>
#include <vector> #include <vector>
#include <memory> #include <memory>
#include "interfaces/Process.hpp"
#include "interfaces/Serializable.hpp"
namespace scripting {
class IClientProjectScript;
}
struct Permissions { struct Permissions {
static inline std::string DEBUGGING = "debugging"; static inline std::string DEBUGGING = "debugging";
static inline std::string NETWORK = "network"; static inline std::string NETWORK = "network";
@ -27,15 +22,10 @@ struct Project : Serializable {
std::string name; std::string name;
std::string title; std::string title;
std::vector<std::string> basePacks; std::vector<std::string> basePacks;
std::unique_ptr<scripting::IClientProjectScript> clientScript;
std::unique_ptr<Process> setupCoroutine;
Permissions permissions; Permissions permissions;
~Project(); ~Project();
dv::value serialize() const override; dv::value serialize() const override;
void deserialize(const dv::value& src) override; void deserialize(const dv::value& src) override;
void loadProjectClientScript();
void loadProjectStartScript();
}; };

View file

@ -13,6 +13,7 @@
#include "content/ContentControl.hpp" #include "content/ContentControl.hpp"
#include "core_defs.hpp" #include "core_defs.hpp"
#include "debug/Logger.hpp" #include "debug/Logger.hpp"
#include "devtools/AppScriptsControl.hpp"
#include "devtools/DebuggingServer.hpp" #include "devtools/DebuggingServer.hpp"
#include "devtools/Editor.hpp" #include "devtools/Editor.hpp"
#include "devtools/Project.hpp" #include "devtools/Project.hpp"
@ -191,10 +192,8 @@ void Engine::initialize(CoreParameters coreParameters) {
audio::set_input_device(name == "auto" ? "" : name); audio::set_input_device(name == "auto" ? "" : name);
})); }));
project->loadProjectStartScript(); appScripts = std::make_unique<AppScriptsControl>(params, *project);
if (!params.headless) {
project->loadProjectClientScript();
}
if (params.stdinCommands) { if (params.stdinCommands) {
cmd::start_stdin_cmd_reader(*this); cmd::start_stdin_cmd_reader(*this);
} }
@ -263,9 +262,7 @@ void Engine::detachDebugger() {
} }
void Engine::applicationTick() { void Engine::applicationTick() {
if (project->setupCoroutine && project->setupCoroutine->isActive()) { appScripts->tick();
project->setupCoroutine->update();
}
} }
void Engine::updateFrontend() { void Engine::updateFrontend() {
@ -353,6 +350,7 @@ void Engine::close() {
network.reset(); network.reset();
clearKeepedObjects(); clearKeepedObjects();
project.reset(); project.reset();
appScripts.reset();
scripting::close(); scripting::close();
logger.info() << "scripting finished"; logger.info() << "scripting finished";
if (!params.headless) { if (!params.headless) {
@ -387,8 +385,8 @@ void Engine::loadProject() {
} }
void Engine::setScreen(std::shared_ptr<Screen> screen) { void Engine::setScreen(std::shared_ptr<Screen> screen) {
if (project->clientScript && this->screen) { if (this->screen) {
project->clientScript->onScreenChange(this->screen->getName(), false); appScripts->onScreenChange(this->screen->getName(), false);
} }
// reset audio channels (stop all sources) // reset audio channels (stop all sources)
audio::reset_channel(audio::get_channel_index("regular")); audio::reset_channel(audio::get_channel_index("regular"));
@ -397,8 +395,8 @@ void Engine::setScreen(std::shared_ptr<Screen> screen) {
if (this->screen) { if (this->screen) {
this->screen->onOpen(); this->screen->onOpen();
} }
if (project->clientScript && this->screen) { if (this->screen) {
project->clientScript->onScreenChange(this->screen->getName(), true); appScripts->onScreenChange(this->screen->getName(), true);
window->setShouldRefresh(); window->setShouldRefresh();
} }
} }

View file

@ -12,6 +12,7 @@
class Assets; class Assets;
class AssetsLoader; class AssetsLoader;
class AssetsManagement; class AssetsManagement;
class AppScriptsControl;
class ContentControl; class ContentControl;
class EngineController; class EngineController;
class EnginePaths; class EnginePaths;
@ -55,6 +56,7 @@ class Engine : public util::ObjectsKeeper {
std::unique_ptr<Project> project; std::unique_ptr<Project> project;
std::unique_ptr<SettingsHandler> settingsHandler; std::unique_ptr<SettingsHandler> settingsHandler;
std::unique_ptr<AssetsManagement> assets; std::unique_ptr<AssetsManagement> assets;
std::unique_ptr<AppScriptsControl> appScripts;
std::shared_ptr<Screen> screen; std::shared_ptr<Screen> screen;
std::unique_ptr<ContentControl> content; std::unique_ptr<ContentControl> content;
std::unique_ptr<EngineController> controller; std::unique_ptr<EngineController> controller;
@ -184,5 +186,9 @@ public:
return debuggingServer.get(); return debuggingServer.get();
} }
AppScriptsControl& getAppScripts() {
return *appScripts;
}
void detachDebugger(); void detachDebugger();
}; };

View file

@ -1,14 +1,17 @@
#include "Mainloop.hpp" #include "Mainloop.hpp"
#include "Engine.hpp"
#include "debug/Logger.hpp" #include "debug/Logger.hpp"
#include "devtools/AppScriptsControl.hpp"
#include "devtools/Project.hpp" #include "devtools/Project.hpp"
#include "frontend/screens/MenuScreen.hpp" #include "Engine.hpp"
#include "frontend/screens/LevelScreen.hpp" #include "frontend/screens/LevelScreen.hpp"
#include "frontend/screens/MenuScreen.hpp"
#include "graphics/ui/elements/Container.hpp"
#include "graphics/ui/GUI.hpp"
#include "io/path.hpp"
#include "logic/scripting/scripting.hpp"
#include "window/Window.hpp" #include "window/Window.hpp"
#include "world/Level.hpp" #include "world/Level.hpp"
#include "graphics/ui/GUI.hpp"
#include "graphics/ui/elements/Container.hpp"
static debug::Logger logger("mainloop"); static debug::Logger logger("mainloop");
@ -19,6 +22,7 @@ void Mainloop::run() {
auto& time = engine.getTime(); auto& time = engine.getTime();
auto& window = engine.getWindow(); auto& window = engine.getWindow();
auto& settings = engine.getSettings(); auto& settings = engine.getSettings();
const auto& coreParams = engine.getCoreParameters();
engine.setLevelConsumer([this](auto level, int64_t localPlayer) { engine.setLevelConsumer([this](auto level, int64_t localPlayer) {
if (level == nullptr) { if (level == nullptr) {
@ -35,9 +39,11 @@ void Mainloop::run() {
logger.info() << "starting menu screen"; logger.info() << "starting menu screen";
engine.setScreen(std::make_shared<MenuScreen>(engine)); engine.setScreen(std::make_shared<MenuScreen>(engine));
auto& appScripts = engine.getAppScripts();
logger.info() << "main loop started"; logger.info() << "main loop started";
while (!window.isShouldClose()){ while (!window.isShouldClose()) {
time.update(window.time()); time.update(window.time());
engine.applicationTick(); engine.applicationTick();
engine.updateFrontend(); engine.updateFrontend();
@ -50,6 +56,10 @@ void Mainloop::run() {
settings.display.adaptiveFpsInMenu.get() && settings.display.adaptiveFpsInMenu.get() &&
dynamic_cast<const MenuScreen*>(engine.getScreen().get()) != nullptr dynamic_cast<const MenuScreen*>(engine.getScreen().get()) != nullptr
); );
if (coreParams.testMode && appScripts.isFinished()) {
logger.info() << "test finished";
engine.quit();
}
} }
logger.info() << "main loop stopped"; logger.info() << "main loop stopped";
} }

View file

@ -1,13 +1,14 @@
#include "ServerMainloop.hpp" #include "ServerMainloop.hpp"
#include "Engine.hpp" #include "Engine.hpp"
#include "logic/scripting/scripting.hpp" #include "devtools/AppScriptsControl.hpp"
#include "logic/LevelController.hpp" #include "logic/LevelController.hpp"
#include "interfaces/Process.hpp" #include "interfaces/Process.hpp"
#include "debug/Logger.hpp" #include "debug/Logger.hpp"
#include "world/Level.hpp" #include "world/Level.hpp"
#include "world/World.hpp" #include "world/World.hpp"
#include "util/platform.hpp" #include "util/platform.hpp"
#include "devtools/Project.hpp"
#include <chrono> #include <chrono>
@ -32,21 +33,12 @@ void ServerMainloop::run() {
setLevel(std::move(level)); setLevel(std::move(level));
}); });
auto process = scripting::start_app_script(
"script:" + coreParams.scriptFile.filename().u8string()
);
double targetDelta = 1.0 / static_cast<double>(coreParams.tps); double targetDelta = 1.0 / static_cast<double>(coreParams.tps);
double delta = targetDelta; double delta = targetDelta;
auto begin = system_clock::now(); auto begin = system_clock::now();
auto startupTime = begin; auto startupTime = begin;
while (process->isActive()) { while (!engine.isQuitSignal() && !engine.getAppScripts().isFinished()) {
if (engine.isQuitSignal()) {
process->terminate();
logger.info() << "script has been terminated due to quit signal";
break;
}
if (coreParams.testMode) { if (coreParams.testMode) {
time.step(delta); time.step(delta);
} else { } else {
@ -55,7 +47,6 @@ void ServerMainloop::run() {
duration_cast<microseconds>(now - startupTime).count() / 1e6); duration_cast<microseconds>(now - startupTime).count() / 1e6);
delta = time.getDelta(); delta = time.getDelta();
} }
process->update();
if (controller) { if (controller) {
controller->getLevel()->getWorld()->updateTimers(delta); controller->getLevel()->getWorld()->updateTimers(delta);
controller->update(glm::min(delta, 0.2), false); controller->update(glm::min(delta, 0.2), false);

View file

@ -7,6 +7,9 @@
#include "graphics/ui/elements/InventoryView.hpp" #include "graphics/ui/elements/InventoryView.hpp"
#include "graphics/ui/gui_xml.hpp" #include "graphics/ui/gui_xml.hpp"
#include "logic/scripting/scripting.hpp" #include "logic/scripting/scripting.hpp"
#include "debug/Logger.hpp"
static debug::Logger logger("ui-document");
UiDocument::UiDocument( UiDocument::UiDocument(
std::string id, std::string id,
@ -17,6 +20,19 @@ UiDocument::UiDocument(
rebuildIndices(); rebuildIndices();
} }
UiDocument::~UiDocument() {
try {
scripting::on_ui_destroy(*this);
} catch (const std::exception& err) {
logger.error() << "an error occurred on calling on_destroy event for document '"
<< id << "': " << err.what();
} catch (...) {
logger.error() << "unknown exception caught on calling on_destroy "
"event for document '"
<< id << "'";
}
}
void UiDocument::rebuildIndices() { void UiDocument::rebuildIndices() {
map.clear(); map.clear();
gui::UINode::getIndices(root, map); gui::UINode::getIndices(root, map);

View file

@ -17,6 +17,7 @@ struct UiDocScript {
bool onopen : 1; bool onopen : 1;
bool onprogress : 1; bool onprogress : 1;
bool onclose : 1; bool onclose : 1;
bool ondestroy : 1;
}; };
using UINodesMap = std::unordered_map<std::string, std::weak_ptr<gui::UINode>>; using UINodesMap = std::unordered_map<std::string, std::weak_ptr<gui::UINode>>;
@ -35,6 +36,8 @@ public:
scriptenv env scriptenv env
); );
~UiDocument();
void rebuildIndices(); void rebuildIndices();
void pushIndices(const std::shared_ptr<gui::UINode>& node); void pushIndices(const std::shared_ptr<gui::UINode>& node);

View file

@ -563,7 +563,7 @@ void Hud::add(const HudElement& element, const dv::value& argsArray) {
args.emplace_back(static_cast<integer_t>(blockPos[i])); args.emplace_back(static_cast<integer_t>(blockPos[i]));
} }
scripting::on_ui_open( scripting::on_ui_open(
element.getDocument(), *element.getDocument(),
std::move(args) std::move(args)
); );
} }
@ -578,7 +578,7 @@ void Hud::onRemove(const HudElement& element) {
if (invview) { if (invview) {
inventory = invview->getInventory().get(); inventory = invview->getInventory().get();
} }
scripting::on_ui_close(document, inventory); scripting::on_ui_close(*document, inventory);
if (invview) { if (invview) {
invview->unbind(); invview->unbind();
} }

View file

@ -86,7 +86,7 @@ UiDocument* menus::show(
); );
auto document = documentPtr.get(); auto document = documentPtr.get();
engine.getAssets()->store(std::move(documentPtr), fullname); engine.getAssets()->store(std::move(documentPtr), fullname);
scripting::on_ui_open(document, std::move(args)); scripting::on_ui_open(*document, std::move(args));
menu->addPage(name, document->getRoot()); menu->addPage(name, document->getRoot());
menu->setPage(name); menu->setPage(name);
return document; return document;
@ -99,13 +99,13 @@ void menus::show_process_panel(
auto menu = engine.getGUI().getMenu(); auto menu = engine.getGUI().getMenu();
menu->reset(); menu->reset();
auto doc = auto document =
menus::show(engine, "process", {util::wstr2str_utf8(langs::get(text))}); menus::show(engine, "process", {util::wstr2str_utf8(langs::get(text))});
std::dynamic_pointer_cast<Container>(doc->getRoot()) std::dynamic_pointer_cast<Container>(document->getRoot())
->listenInterval(0.01f, [=]() { ->listenInterval(0.01f, [=]() {
task->update(); task->update();
uint tasksDone = task->getWorkDone(); uint tasksDone = task->getWorkDone();
scripting::on_ui_progress(doc, tasksDone, initialWork); scripting::on_ui_progress(*document, tasksDone, initialWork);
}); });
} }

View file

@ -26,6 +26,11 @@ Font::Font(
pages(std::move(pages)), pages(std::move(pages)),
glyphs(std::move(glyphs)), glyphs(std::move(glyphs)),
fontFile(std::move(fontFile)) { fontFile(std::move(fontFile)) {
if (this->fontFile.has_value()) {
if (auto fontPtr = this->fontFile->lock()) {
monospace = fontPtr->isMonospace();
}
}
} }
Font::~Font() = default; Font::~Font() = default;
@ -34,6 +39,10 @@ int Font::getYOffset() const {
return yoffset; return yoffset;
} }
bool Font::isMonospace() const {
return monospace;
}
int Font::getLineHeight() const { int Font::getLineHeight() const {
return lineHeight; return lineHeight;
} }
@ -53,7 +62,7 @@ bool Font::isPrintableChar(uint codepoint) const {
int FontMetrics::calcWidth(std::wstring_view text, size_t offset, size_t length) const { int FontMetrics::calcWidth(std::wstring_view text, size_t offset, size_t length) const {
auto font = this->font.has_value() ? this->font->lock() : nullptr; auto font = this->font.has_value() ? this->font->lock() : nullptr;
if (font == nullptr) { if (font == nullptr || font->isMonospace()) {
return std::min(text.length() - offset, length) * _glyphInterval; return std::min(text.length() - offset, length) * _glyphInterval;
} }
int totalWidth = 0; int totalWidth = 0;
@ -153,6 +162,8 @@ static inline void draw_text(
const FontStylesScheme* styles, const FontStylesScheme* styles,
size_t styleMapOffset size_t styleMapOffset
) { ) {
bool is3d = std::is_same<Batch, Batch3D>();
static FontStylesScheme defStyles {{{}}, {0}}; static FontStylesScheme defStyles {{{}}, {0}};
if (styles == nullptr) { if (styles == nullptr) {
@ -185,13 +196,26 @@ static inline void draw_text(
float advance = baseAdvance; float advance = baseAdvance;
if (auto glyph = font.getGlyph(c)) { if (auto glyph = font.getGlyph(c)) {
yOffset = glyph->yOffset; yOffset = glyph->yOffset;
advance = glyph->xAdvance; advance = glyph->xAdvance /
static_cast<float>(font.getLineHeight()) * 2.0f *
baseAdvance;
} }
uint charpage = c >> 8; uint charpage = c >> 8;
if (charpage == page){ if (charpage == page){
batch.texture(font.getPage(charpage)); batch.texture(font.getPage(charpage));
draw_glyph( draw_glyph(
batch, pos, glm::vec2(x, y - yOffset / static_cast<float>(font.getLineHeight())), c, right, up, interval, style batch,
pos,
glm::vec2(
x,
y - yOffset * (is3d ? -1 : 1) /
static_cast<float>(font.getLineHeight())
),
c,
right,
up,
interval,
style
); );
} }
else if (charpage > page && charpage < next){ else if (charpage > page && charpage < next){

View file

@ -39,6 +39,7 @@ public:
); );
~Font(); ~Font();
bool isMonospace() const;
int getLineHeight() const; int getLineHeight() const;
int getYOffset() const; int getYOffset() const;
@ -94,6 +95,7 @@ private:
int lineHeight; int lineHeight;
int yoffset; int yoffset;
int glyphInterval; int glyphInterval;
bool monospace = true;
std::vector<std::unique_ptr<Texture>> pages; std::vector<std::unique_ptr<Texture>> pages;
std::vector<Glyph> glyphs; std::vector<Glyph> glyphs;
std::optional<std::weak_ptr<vector_fonts::FontFile>> fontFile; std::optional<std::weak_ptr<vector_fonts::FontFile>> fontFile;

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

@ -58,7 +58,10 @@ void TextsRenderer::renderNote(
} }
opacity = preset.xrayOpacity; opacity = preset.xrayOpacity;
} }
auto& font = assets.require<Font>(FONT_DEFAULT);
auto specifiedFont = assets.get<Font>(preset.font);
auto& font =
specifiedFont ? *specifiedFont : assets.require<Font>(FONT_DEFAULT);
glm::vec3 xvec = note.getAxisX(); glm::vec3 xvec = note.getAxisX();
glm::vec3 yvec = note.getAxisY(); glm::vec3 yvec = note.getAxisY();
@ -76,7 +79,10 @@ void TextsRenderer::renderNote(
yvec = camera.up; yvec = camera.up;
} }
float scale = float scale =
(1.0f - preset.perspective) * glm::pow(glm::distance(camera.position, pos), 1.0f-preset.perspective); (1.0f - preset.perspective) *
glm::pow(
glm::distance(camera.position, pos), 1.0f - preset.perspective
);
xvec *= 1.0f + scale; xvec *= 1.0f + scale;
yvec *= 1.0f + scale; yvec *= 1.0f + scale;
} }
@ -118,6 +124,7 @@ void TextsRenderer::renderNote(
} }
auto color = preset.color; auto color = preset.color;
batch.setColor(glm::vec4(color.r, color.g, color.b, color.a * opacity)); batch.setColor(glm::vec4(color.r, color.g, color.b, color.a * opacity));
font.draw( font.draw(
batch, batch,
text, text,

View file

@ -13,7 +13,7 @@ InlineFrame::~InlineFrame() = default;
void InlineFrame::setSrc(const std::string& src) { void InlineFrame::setSrc(const std::string& src) {
this->src = src; this->src = src;
if (document) { if (document) {
scripting::on_ui_close(document.get(), nullptr); scripting::on_ui_close(*document, nullptr);
document = nullptr; document = nullptr;
root = nullptr; root = nullptr;
} }
@ -31,7 +31,7 @@ void InlineFrame::setDocument(const std::shared_ptr<UiDocument>& document) {
root->setSize(size); root->setSize(size);
gui.postRunnable([this]() { gui.postRunnable([this]() {
scripting::on_ui_open(this->document.get(), {}); scripting::on_ui_open(*this->document, {});
}); });
} }

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

@ -173,8 +173,12 @@ bool io::write_binary_json(
} }
dv::value io::read_json(const path& filename) { dv::value io::read_json(const path& filename) {
std::string text = io::read_string(filename); auto text = io::read_string(filename);
return json::parse(filename.string(), text); try {
return json::parse(filename.string(), text);
} catch (const parsing_error& err) {
throw err.toRuntimeError();
}
} }
dv::value io::read_binary_json(const path& file) { dv::value io::read_binary_json(const path& file) {
@ -184,7 +188,12 @@ dv::value io::read_binary_json(const path& file) {
} }
dv::value io::read_toml(const path& file) { dv::value io::read_toml(const path& file) {
return toml::parse(file.string(), io::read_string(file)); auto text = io::read_string(file);
try {
return toml::parse(file.string(), text);
} catch (const parsing_error& err) {
throw err.toRuntimeError();
}
} }
std::vector<std::string> io::read_list(const io::path& filename) { std::vector<std::string> io::read_list(const io::path& filename) {

View file

@ -15,8 +15,33 @@ public:
memory_streambuf(const memory_streambuf&) = delete; memory_streambuf(const memory_streambuf&) = delete;
memory_streambuf& operator=(const memory_streambuf&) = delete; memory_streambuf& operator=(const memory_streambuf&) = delete;
pos_type seekoff(off_type off, std::ios_base::seekdir way,
std::ios_base::openmode mode = std::ios_base::in) override {
if (!(mode & std::ios_base::in)) return pos_type(off_type(-1));
char* base = eback();
char* end = egptr();
if (!base || !end) return pos_type(off_type(-1));
std::ptrdiff_t current = gptr() - base;
std::ptrdiff_t newpos;
if (way == std::ios_base::beg) newpos = off;
else if (way == std::ios_base::cur) newpos = current + off;
else if (way == std::ios_base::end) newpos = (end - base) + off;
else return pos_type(off_type(-1));
if (newpos < 0 || base + newpos > end) return pos_type(off_type(-1));
setg(base, base + newpos, end);
return pos_type(off_type(newpos));
}
pos_type seekpos(pos_type sp, std::ios_base::openmode mode = std::ios_base::in) override {
return seekoff(off_type(sp), std::ios_base::beg, mode);
}
protected: protected:
int_type underflow() override { int_type underflow() override {
if (gptr() < egptr()) {
return traits_type::to_int_type(*gptr());
}
return traits_type::eof(); return traits_type::eof();
} }
@ -36,7 +61,7 @@ private:
class memory_view_streambuf : public std::streambuf { class memory_view_streambuf : public std::streambuf {
public: public:
explicit memory_view_streambuf(const util::Buffer<char>& buffer) explicit memory_view_streambuf(const util::Buffer<char>& buffer)
: buffer(std::move(buffer)) { : buffer(buffer) {
char* base = const_cast<char*>(this->buffer.data()); char* base = const_cast<char*>(this->buffer.data());
char* end = base + this->buffer.size(); char* end = base + this->buffer.size();
setg(base, base, end); setg(base, base, end);
@ -45,8 +70,33 @@ public:
memory_view_streambuf(const memory_view_streambuf&) = delete; memory_view_streambuf(const memory_view_streambuf&) = delete;
memory_view_streambuf& operator=(const memory_view_streambuf&) = delete; memory_view_streambuf& operator=(const memory_view_streambuf&) = delete;
pos_type seekoff(off_type off, std::ios_base::seekdir way,
std::ios_base::openmode mode = std::ios_base::in) override {
if (!(mode & std::ios_base::in)) return pos_type(off_type(-1));
char* base = eback();
char* end = egptr();
if (!base || !end) return pos_type(off_type(-1));
std::ptrdiff_t current = gptr() - base;
std::ptrdiff_t newpos;
if (way == std::ios_base::beg) newpos = off;
else if (way == std::ios_base::cur) newpos = current + off;
else if (way == std::ios_base::end) newpos = (end - base) + off;
else return pos_type(off_type(-1));
if (newpos < 0 || base + newpos > end) return pos_type(off_type(-1));
setg(base, base + newpos, end);
return pos_type(off_type(newpos));
}
pos_type seekpos(pos_type sp, std::ios_base::openmode mode = std::ios_base::in) override {
return seekoff(off_type(sp), std::ios_base::beg, mode);
}
protected: protected:
int_type underflow() override { int_type underflow() override {
if (gptr() < egptr()) {
return traits_type::to_int_type(*gptr());
}
return traits_type::eof(); return traits_type::eof();
} }

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

@ -1,104 +0,0 @@
#include "logic/scripting/descriptors_manager.hpp"
#include "debug/Logger.hpp"
static debug::Logger logger("descriptors-manager");
namespace scripting {
std::vector<std::optional<StreamDescriptor>> descriptors_manager::descriptors;
std::istream* descriptors_manager::get_input(int descriptor) {
if (!is_readable(descriptor))
return nullptr;
return descriptors[descriptor]->in.get();
}
std::ostream* descriptors_manager::get_output(int descriptor) {
if (!is_writeable(descriptor))
return nullptr;
return descriptors[descriptor]->out.get();
}
void descriptors_manager::flush(int descriptor) {
if (is_writeable(descriptor)) {
descriptors[descriptor]->out->flush();
}
}
bool descriptors_manager::has_descriptor(int descriptor) {
return is_readable(descriptor) || is_writeable(descriptor);
}
bool descriptors_manager::is_readable(int descriptor) {
return descriptor >= 0 && descriptor < static_cast<int>(descriptors.size())
&& descriptors[descriptor].has_value()
&& descriptors[descriptor]->in != nullptr;
}
bool descriptors_manager::is_writeable(int descriptor) {
return descriptor >= 0 && descriptor < static_cast<int>(descriptors.size())
&& descriptors[descriptor].has_value()
&& descriptors[descriptor]->out != nullptr;
}
void descriptors_manager::close(int descriptor) {
if (descriptor >= 0 && descriptor < static_cast<int>(descriptors.size())) {
if (descriptors[descriptor].has_value()) {
auto& desc = descriptors[descriptor].value();
if (desc.out)
desc.out->flush();
desc.in.reset();
desc.out.reset();
}
descriptors[descriptor].reset();
descriptors[descriptor] = std::nullopt;
}
}
int descriptors_manager::open_descriptor(const io::path& path, bool write, bool read) {
std::unique_ptr<std::istream> in;
std::unique_ptr<std::ostream> out;
try {
if (read)
in = io::read(path);
if (write)
out = io::write(path);
} catch (const std::exception& e) {
logger.error() << "failed to open descriptor for " << path.string()
<< ": " << e.what();
return -1;
}
for (int i = 0; i < static_cast<int>(descriptors.size()); ++i) {
if (!descriptors[i].has_value()) {
descriptors[i] = StreamDescriptor{ std::move(in), std::move(out) };
return i;
}
}
descriptors.emplace_back(StreamDescriptor{ std::move(in), std::move(out) });
return static_cast<int>(descriptors.size() - 1);
}
void descriptors_manager::close_all_descriptors() {
for (int i = 0; i < static_cast<int>(descriptors.size()); ++i) {
if (descriptors[i].has_value()) {
close(i);
}
}
descriptors.clear();
}
}

View file

@ -1,39 +0,0 @@
#pragma once
#include <memory>
#include <vector>
#include <optional>
#include <string>
#include <istream>
#include <ostream>
#include "io/io.hpp"
namespace scripting {
struct StreamDescriptor {
std::unique_ptr<std::istream> in;
std::unique_ptr<std::ostream> out;
};
class descriptors_manager {
private:
static std::vector<std::optional<StreamDescriptor>> descriptors;
public:
static std::istream* get_input(int descriptor);
static std::ostream* get_output(int descriptor);
static void flush(int descriptor);
static bool has_descriptor(int descriptor);
static bool is_readable(int descriptor);
static bool is_writeable(int descriptor);
static void close(int descriptor);
static int open_descriptor(const io::path& path, bool write, bool read);
static void close_all_descriptors();
};
}

View file

@ -0,0 +1,160 @@
#include "logic/scripting/io_descriptors.hpp"
#include "debug/Logger.hpp"
#include "io/io.hpp"
#include <memory>
#include <string>
#include <vector>
#include <optional>
static debug::Logger logger("descriptors-manager");
using namespace scripting;
namespace {
struct StreamDescriptor {
// TODO: std::iostream?
std::unique_ptr<std::istream> in;
std::unique_ptr<std::ostream> out;
};
std::vector<std::optional<StreamDescriptor>> descriptors;
}
std::istream* io_descriptors::get_input(int id) {
if (!is_readable(id)) {
return nullptr;
}
return ::descriptors[id]->in.get();
}
std::ostream* io_descriptors::get_output(int id) {
if (!is_writeable(id)) {
return nullptr;
}
return ::descriptors[id]->out.get();
}
static StreamDescriptor& require_descriptor(int id) {
if (!io_descriptors::has_descriptor(id)) {
throw std::runtime_error(
"io-descriptor with id " + std::to_string(id) + " does not exists"
);
}
return *::descriptors[id];
}
std::istream& io_descriptors::require_input(int id) {
const auto& descriptor = require_descriptor(id);
if (descriptor.in) {
return *descriptor.in;
}
throw std::runtime_error("io-descriptor is not readable");
}
std::ostream& io_descriptors::require_output(int id) {
const auto& descriptor = require_descriptor(id);
if (descriptor.out) {
return *descriptor.out;
}
throw std::runtime_error("io-descriptor is not writeable");
}
void io_descriptors::flush(int id) {
if (is_writeable(id)) {
::descriptors[id]->out->flush();
}
}
int io_descriptors::available(int id) {
if (!is_readable(id))
return 0;
auto* stream = descriptors[id]->in.get();
auto current_pos = stream->tellg();
if (current_pos == -1) {
return 0;
}
stream->seekg(0, std::ios::end);
auto end_pos = stream->tellg();
stream->seekg(current_pos, std::ios::beg);
auto remaining = end_pos - current_pos;
if (remaining > std::numeric_limits<int>::max()) {
return std::numeric_limits<int>::max();
}
return static_cast<int>(remaining);
}
bool io_descriptors::has_descriptor(int id) {
return id >= 0 && id < static_cast<int>(::descriptors.size()) &&
::descriptors[id].has_value();
}
bool io_descriptors::is_readable(int id) {
return has_descriptor(id) && ::descriptors[id]->in != nullptr;
}
bool io_descriptors::is_writeable(int id) {
return id >= 0 && id < static_cast<int>(::descriptors.size())
&& ::descriptors[id].has_value()
&& ::descriptors[id]->out != nullptr;
}
void io_descriptors::close(int id) {
if (!has_descriptor(id)) {
return;
}
auto& desc = ::descriptors[id].value();
if (desc.out) {
desc.out->flush();
}
desc.in.reset();
desc.out.reset();
::descriptors[id] = std::nullopt;
}
int io_descriptors::open_descriptor(const io::path& path, bool write, bool read) {
std::unique_ptr<std::istream> in;
std::unique_ptr<std::ostream> out;
try {
if (read) {
in = io::read(path);
}
if (write) {
out = io::write(path);
}
} catch (const std::exception& e) {
logger.error() << "failed to open descriptor for " << path.string()
<< ": " << e.what();
return -1;
}
for (int i = 0; i < static_cast<int>(descriptors.size()); ++i) {
if (!descriptors[i].has_value()) {
descriptors[i] = StreamDescriptor{ std::move(in), std::move(out) };
return i;
}
}
::descriptors.emplace_back(StreamDescriptor{ std::move(in), std::move(out) });
return static_cast<int>(::descriptors.size() - 1);
}
void io_descriptors::close_all_descriptors() {
for (int i = 0; i < static_cast<int>(::descriptors.size()); ++i) {
if (::descriptors[i].has_value()) {
close(i);
}
}
::descriptors.clear();
}

View file

@ -0,0 +1,27 @@
#pragma once
#include <istream>
#include <ostream>
#include "io/fwd.hpp"
namespace scripting::io_descriptors {
std::istream* get_input(int id);
std::ostream* get_output(int id);
std::istream& require_input(int id);
std::ostream& require_output(int id);
void flush(int id);
int available(int id);
bool has_descriptor(int id);
bool is_readable(int id);
bool is_writeable(int id);
void close(int id);
int open_descriptor(const io::path& path, bool write, bool read);
void close_all_descriptors();
}

View file

@ -44,6 +44,7 @@ extern const luaL_Reg posteffectslib[]; // gfx.posteffects
extern const luaL_Reg quatlib[]; extern const luaL_Reg quatlib[];
extern const luaL_Reg randomlib[]; extern const luaL_Reg randomlib[];
extern const luaL_Reg compressionlib[]; extern const luaL_Reg compressionlib[];
extern const luaL_Reg testlib[];
extern const luaL_Reg text3dlib[]; // gfx.text3d extern const luaL_Reg text3dlib[]; // gfx.text3d
extern const luaL_Reg timelib[]; extern const luaL_Reg timelib[];
extern const luaL_Reg tomllib[]; extern const luaL_Reg tomllib[];
@ -53,6 +54,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);

Some files were not shown because too many files have changed in this diff Show more