Merge branch 'MihailRis:main' into main

This commit is contained in:
Astazium 2026-08-04 17:15:26 +03:00 • committed by GitHub
commit 7536ac2736
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
130 changed files with 2932 additions and 1028 deletions

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)

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

@ -1,6 +1,6 @@
# Block properties # Block properties
## Visual ## Visual & Audio
### *texture* ### *texture*
@ -70,6 +70,18 @@ Rotation profile (set of available block rotations and behaviour of placing bloc
- "pane" - panels, doors, signs - "pane" - panels, doors, signs
- "stairs" - "pane" + flipped variants - "stairs" - "pane" + flipped variants
### *particles*
Particles are specified as a JSON object. Property names can be found [in the particles section](particles.md).
When camera is near to the block, the engine will create an emitter that will run
until the block is destroyed or the camera moves away a certain distance.
### Material - *material*
Defines the name of the block's material in the format `pack:material_name`, which affects the selection of block interaction sounds.
Material definitions are located in /block_materials.
## Variants ## Variants
Some properties can vary dynamically, depending on the variant number stored in the user bits of the block. Some properties can vary dynamically, depending on the variant number stored in the user bits of the block.

View file

@ -61,12 +61,16 @@ Determines how the physics engine will work with it.
### *blocking* ### *blocking*
Determines whether the entity blocks installation of blocks. If true - the entity blocks the placement of a block that intersects with the entity's hitbox.
*In the future will also block other entities movement.* *In the future will also block other entities movement.*
Default value: *true*. Default value: *true*.
### *selectable*
If set to `false` the cursor will ignore the entity, passing the ray.
### *sensors* ### *sensors*
A sensor is an area attached to a physical body that detects the entry of other bodies into it. A sensor is an area attached to a physical body that detects the entry of other bodies into it.

View file

@ -38,7 +38,7 @@ Models are loaded automatically; adding them to preload.json is not required.
## Models ## Models
Models should be located in the models folder. Currently only OBJ format is supported. Models must be located in the models folder. Currently, the following formats are supported: obj, vec3, and [vcm](vcm.md).
>[!IMPORTANT] >[!IMPORTANT]
> When loading an obj model, the \*.mtl file is ignored. > When loading an obj model, the \*.mtl file is ignored.

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)

View file

@ -180,14 +180,34 @@ app.get_content_sources() -> table<string>
Returns a list of content sources (paths), in descending priority order. Returns a list of content sources (paths), in descending priority order.
``lua ```lua
app.set_content_sources(sources: table<string>) app.set_content_sources(sources: table<string>)
``` ```
Sets a list of content sources (paths). Specified in descending priority order. Sets a list of content sources (paths). Specified in descending priority order.
``lua ```lua
app.reset_content_sources() app.reset_content_sources()
``` ```
Resets content sources. Resets content sources.
## Sub-instances
```lua
-- Creates a headless engine instance with the current project and the specified application script.
-- Returns the instance ID. The number of active sub-instances is currently limited to one.
app.start_background_instance(
-- script file
app_script: string,
-- log file
output_file: string | nil
) -> int
-- Checks if the engine sub-instance is alive.
app.is_instance_alive(handle: int) -> boolean
-- Stops the engine sub-instance.
-- Returns true if the instance was alive at the time of the call.
app.terminate(handle: int) -> boolean
```

View file

@ -28,7 +28,8 @@ block.get(x: int, y: int, z: int) -> int
block.get_states(x: int, y: int, z: int) -> int block.get_states(x: int, y: int, z: int) -> int
-- Set block with given integer ID and state (default - 0) at given position. -- Set block with given integer ID and state (default - 0) at given position.
block.set(x: int, y: int, z: int, id: int, states: int) -- If noupdate=true is passed, the `on_update` event will not be called for adjacent blocks.
block.set(x: int, y: int, z: int, id: int, states: int, noupdate: boolean=false)
-- Places a block with a given integer id and state (default - 0) at given position. -- Places a block with a given integer id and state (default - 0) at given position.
-- on behalf of the player, calling the on_placed event. -- on behalf of the player, calling the on_placed event.

View file

@ -92,6 +92,8 @@ world.raycast(
-- exclusion filtering mode: -- exclusion filtering mode:
-- if true, filter_entities defines the list of ignored entity types -- if true, filter_entities defines the list of ignored entity types
entities_exclusion: bool = false, entities_exclusion: bool = false,
-- include entities with selectable=false
nonselect_entities: bool = false;
[==[ additional parameters (blocks) ]==] [==[ additional parameters (blocks) ]==]
-- id of the block types used for filtering -- id of the block types used for filtering

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

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

View file

@ -1,6 +1,6 @@
# Свойства блоков # Свойства блоков
## Вид ## Вид & Звук
### Текстура - *texture* ### Текстура - *texture*
@ -78,6 +78,11 @@
При приближении к блоку движок создаст эмиттер, который будет работать При приближении к блоку движок создаст эмиттер, который будет работать
до разрушения блока или отдаления камеры на некоторое расстояние. до разрушения блока или отдаления камеры на некоторое расстояние.
### Материал - *material*
Определяет имя материала блока в формате `пак:имя_материала`, что влияет на выбор звуков взаимодействия с блоком.
Определения материалов расположены в /block_materials.
## Варианты ## Варианты
Некоторые свойства могут варьироваться динамически, в зависимости от номера варианта, хранимого в пользовательских битах блока. Некоторые свойства могут варьироваться динамически, в зависимости от номера варианта, хранимого в пользовательских битах блока.

View file

@ -61,12 +61,16 @@
### Блокирование - *blocking* ### Блокирование - *blocking*
Определяет блокирует ли сущность установку блоков. Определяет, блокирует ли сущность установку блока, пересекающегося с хитбоксом сущности.
*В будущем будет также блокировать движение других сущностей.* *В будущем будет также блокировать движение других сущностей.*
Значение по-умолчанию: *true*. Значение по-умолчанию: *true*.
### Выделяемость - *selectable*
При значении в `false` курсор будет игнорировать cущность, пропуская луч далее.
### Список сенсоров - *sensors* ### Список сенсоров - *sensors*
Сенсор - область пространства, привязанная к физическому телу, детектирующее попадание в него других тел. Сенсор - область пространства, привязанная к физическому телу, детектирующее попадание в него других тел.

View file

@ -38,7 +38,7 @@
## Модели ## Модели
Модели должны располагаться в папке models. На данный момент поддерживается только OBJ формат. Модели должны располагаться в папке models. На данный момент поддерживаются форматы: obj, vec3 и [vcm](vcm.md).
>[!IMPORTANT] >[!IMPORTANT]
> При загрузке obj модели игнорируется файл \*.mtl. > При загрузке obj модели игнорируется файл \*.mtl.

View file

@ -143,3 +143,23 @@ app.set_content_sources(sources: table<string>)
-- Сбрасывает список источников контента. -- Сбрасывает список источников контента.
app.reset_content_sources() app.reset_content_sources()
``` ```
## Под-экземпляры
```lua
-- Создаёт headless-экземпляр движка с текущим проектом и указанным сценарием.
-- Возвращает id экземпляра. Число живых под-экземпляров, на данный момент, ограничено одним.
app.start_background_instance(
-- файл сценария
app_script: string,
-- файл лога
output_file: string | nil
) -> int
-- Проверяет, жив ли под-экземпляр движка.
app.is_instance_alive(handle: int) -> boolean
-- Останавливает под-экземпляр движка.
-- Возвращает true если экземпляр был жив в момент вызова.
app.terminate_instance(handle: int) -> boolean
```

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

@ -91,6 +91,8 @@ world.raycast(
-- исключающий режим фильтрации: -- исключающий режим фильтрации:
-- при значении (true) filter_entities определяет список игнорируемых -- при значении (true) filter_entities определяет список игнорируемых
entities_exclusion: bool = false, entities_exclusion: bool = false,
-- учитывать сущности с selectable=false
nonselect_entities: bool = false;
[==[ дополнительные параметры (блоки) ]==] [==[ дополнительные параметры (блоки) ]==]
-- id типов блоков используемых для фильтрации -- id типов блоков используемых для фильтрации

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
``` ```
@ -158,6 +164,12 @@ math.sum(x: number, ... | t: table) -> number
-- Возвращает целое число, указывающее знак числа (-1/0/1) -- Возвращает целое число, указывающее знак числа (-1/0/1)
math.sign(x: number) -> int 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

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,3 +1,4 @@
generator = "base:demo" generator = "base:demo"
player-entity = "base:player" player-entity = "base:player"
hand-skeleton = "base:hand" hand-skeleton = "base:hand"
block-material = "base:stone"

View file

@ -12,7 +12,7 @@ function util.drop(ppos, itemid, count, data, pickup_delay)
}}) }})
end end
local function calc_loot(loot_table) function util.calc_loot(loot_table)
local results = {} local results = {}
for _, loot in ipairs(loot_table) do for _, loot in ipairs(loot_table) do
local chance = loot.chance or 1 local chance = loot.chance or 1
@ -37,7 +37,7 @@ end
function util.block_loot(blockid) function util.block_loot(blockid)
local lootscheme = block.properties[blockid]["base:loot"] local lootscheme = block.properties[blockid]["base:loot"]
if lootscheme then if lootscheme then
return calc_loot(lootscheme) return util.calc_loot(lootscheme)
end end
return {{item=block.get_picking_item(blockid), count=1}} return {{item=block.get_picking_item(blockid), count=1}}
end end

View file

@ -26,6 +26,7 @@ function on_save()
end end
if vc.is_client() then if vc.is_client() then
local SCALE = 0.3
local scale = {1, 1, 1} local scale = {1, 1, 1}
local rotation = mat4.rotate({ local rotation = mat4.rotate({
math.random(), math.random(), math.random() math.random(), math.random(), math.random()
@ -35,7 +36,7 @@ if vc.is_client() then
local matrix = mat4.idt() local matrix = mat4.idt()
rig:set_model(0, item.model_name(dropitem.id)) rig:set_model(0, item.model_name(dropitem.id))
local bodysize = math.min(scale[1], scale[2], scale[3]) local bodysize = math.min(scale[1], scale[2], scale[3])
body:set_size({scale[1], bodysize, scale[3]}) body:set_size(vec3.mul({scale[1], bodysize, scale[3]}, SCALE))
mat4.mul(matrix, rotation, matrix) mat4.mul(matrix, rotation, matrix)
mat4.scale(matrix, scale, matrix) mat4.scale(matrix, scale, matrix)
rig:set_matrix(0, matrix) rig:set_matrix(0, matrix)

View file

@ -1,3 +1,6 @@
local dirtid = block.index("base:dirt")
local grass_blockid = block.index("base:grass_block")
local offsets = {} local offsets = {}
for lx=-1,1 do for lx=-1,1 do
for ly=-1,1 do for ly=-1,1 do
@ -8,18 +11,17 @@ for lx=-1,1 do
end end
function on_random_update(x, y, z) function on_random_update(x, y, z)
local dirtid = block.index('base:dirt');
if block.is_solid_at(x, y+1, z) then if block.is_solid_at(x, y+1, z) then
block.set(x, y, z, dirtid, 0) block.set(x, y, z, dirtid, 0)
return return
end end
local grassblockid = block.index('base:grass_block')
for _, offset in ipairs(offsets) do for _, offset in ipairs(offsets) do
local nx, ny, nz = x + offset[1], y + offset[2], z + offset[3] local nx, ny, nz = x + offset[1], y + offset[2], z + offset[3]
if block.get(nx, ny, nz) == dirtid and if block.get(nx, ny, nz) == dirtid and
not block.is_solid_at(nx, ny + 1, nz) then not block.is_solid_at(nx, ny + 1, nz) then
block.set(nx, ny, nz, grassblockid, 0) block.set(nx, ny, nz, grass_blockid, 0)
return return
end end
end end

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

@ -3,6 +3,7 @@ function on_open(params)
mode = params.mode mode = params.mode
end end
refresh() refresh()
document.search_textbox.focused = true
end end
-- add - packs to be added to the world (after apply) -- add - packs to be added to the world (after apply)
@ -178,40 +179,40 @@ function place_pack(panel, packinfo, callback, position_func)
end end
end end
local Version = {}; local Version = {}
function Version.matches_pattern(version) function Version.matches_pattern(version)
for _, letter in string.gmatch(version, "%.+") do local t = string.split(version, ".")
if type(letter) ~= "number" or letter ~= "." then if #t ~= 2 and #t ~= 3 then return false end
return false;
end
local t = string.split(version, "."); for i = 1, #t do
local matched = string.match(t[i], "^%d+$")
return #t == 2 or #t == 3; if not matched then return false end
end end
return true
end end
function Version.__equal(ver1, ver2) function Version.__equal(ver1, ver2)
return ver1[1] == ver2[1] and ver1[2] == ver2[2] and ver1[3] == ver2[3]; return ver1[1] == ver2[1] and ver1[2] == ver2[2] and ver1[3] == ver2[3]
end end
function Version.__greater(ver1, ver2) function Version.__greater(ver1, ver2)
if ver1[1] ~= ver2[1] then return ver1[1] > ver2[1] end; if ver1[1] ~= ver2[1] then return ver1[1] > ver2[1] end
if ver1[2] ~= ver2[2] then return ver1[2] > ver2[2] end; if ver1[2] ~= ver2[2] then return ver1[2] > ver2[2] end
return ver1[3] > ver2[3]; return ver1[3] > ver2[3]
end end
function Version.__less(ver1, ver2) function Version.__less(ver1, ver2)
return Version.__greater(ver2, ver1); return Version.__greater(ver2, ver1)
end end
function Version.__greater_or_equal(ver1, ver2) function Version.__greater_or_equal(ver1, ver2)
return not Version.__less(ver1, ver2); return not Version.__less(ver2, ver1)
end end
function Version.__less_or_equal(ver1, ver2) function Version.__less_or_equal(ver1, ver2)
return not Version.__greater(ver1, ver2); return not Version.__greater(ver1, ver2)
end end
Version.operators = { Version.operators = {
@ -223,40 +224,39 @@ Version.operators = {
} }
function Version.compare(op, ver1, ver2) function Version.compare(op, ver1, ver2)
ver1 = string.split(ver1, "."); ver1 = string.split(ver1, ".")
ver2 = string.split(ver2, "."); ver2 = string.split(ver2, ".")
local comparison_func = Version.operators[op]; local comparison_func = Version.operators[op];
if comparison_func then if comparison_func then
return comparison_func(ver1, ver2); return comparison_func(ver1, ver2)
else else
return false; return false
end end
end end
function Version.parse(version) function Version.parse(version)
local op = string.sub(version, 1, 2); local op = string.sub(version, 1, 2)
if op == ">=" or op == "<=" then if op == ">=" or op == "<=" then
return op, string.sub(version, #op + 1); return op, string.sub(version, #op + 1)
end end
op = string.sub(version, 1, 1); op = string.sub(version, 1, 1);
if op == ">" or op == "<" then if op == ">" or op == "<" then
return op, string.sub(version, #op + 1); return op, string.sub(version, #op + 1)
end end
return "=", version; return "=", version
end end
local function compare_version(dependent_version, actual_version) local function compare_version(op, dependent_version, actual_version)
if Version.matches_pattern(dependent_version) and Version.matches_pattern(actual_version) then if Version.matches_pattern(dependent_version) and Version.matches_pattern(actual_version) then
local op, dep_ver = Version.parse_version(dependent_version); return Version.compare(op, dep_ver, actual_version)
Version.compare(op, dep_ver, actual_version);
elseif dependent_version == "*" or dependent_version == actual_version then elseif dependent_version == "*" or dependent_version == actual_version then
return true; return true
else else
return false; return false
end end
end end
@ -276,15 +276,15 @@ function check_dependencies(packinfo)
) )
end end
local dep_pack = pack.get_info(depid); local dep_pack = pack.get_info(depid)
local op, ver = Version.parse(depver)
if not compare_version(depver, dep_pack.version) then if not compare_version(op, ver, dep_pack.version) then
local op, ver = Version.parse(depver)
return string.format( return string.format(
"%s: %s != %s (%s)", "%s: %s != %s (%s)",
gui.str("error.dependency-version-not-met"), gui.str("error.dependency-version-not-met"),
dep_pack.version, ver, depid dep_pack.version, ver, depid
); )
end end
if table.has(packs_installed, packinfo.id) then if table.has(packs_installed, packinfo.id) then

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

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

@ -39,3 +39,85 @@ end
function math.sign(x) function math.sign(x)
return (x > 0) and 1 or (x < 0 and -1 or 0) return (x > 0) and 1 or (x < 0 and -1 or 0)
end 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

@ -1,4 +1,4 @@
local rules = {nexid = 1, rules = {}} local rules = {nextid = 1, rules = {}}
function rules.get_rule(name) function rules.get_rule(name)
local rule = rules.rules[name] local rule = rules.rules[name]
@ -27,13 +27,13 @@ end
function rules.reset(name) function rules.reset(name)
local rule = rules.get_rule(name) local rule = rules.get_rule(name)
rules.set(rule.default) rules.set(name, rule.default)
end end
function rules.listen(name, handler) function rules.listen(name, handler)
local rule = rules.get_rule(name) local rule = rules.get_rule(name)
local id = rules.nexid local id = rules.nextid
rules.nextid = rules.nexid + 1 rules.nextid = rules.nextid + 1
rule.listeners[utf8.encode(id)] = handler rule.listeners[utf8.encode(id)] = handler
return id return id
end end

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,14 +4,27 @@ 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
) )

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) local copy = self.read_buffer:slice(1, length)
self.readBuffer:clear() 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

@ -3,5 +3,6 @@ name = "default"
base_packs = ["base"] base_packs = ["base"]
permissions = [ permissions = [
"network", "network",
"record-audio" "record-audio",
"sub-instances",
] ]

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

@ -140,7 +140,7 @@ console.add_command(
for y=y1,y2 do for y=y1,y2 do
for z=z1,z2 do for z=z1,z2 do
for x=x1,x2 do for x=x1,x2 do
block.set(x, y, z, id) block.set(x, y, z, id, 0, true)
end end
end end
end end

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
@ -136,7 +140,20 @@ local __vc_named_coroutines = {}
local __vc_next_coroutine = 1 local __vc_next_coroutine = 1
function __vc_start_coroutine(chunk) function __vc_start_coroutine(chunk)
local co = coroutine.create(chunk) local co = coroutine.create(function()
local _, err = xpcall(chunk, function(msg)
local traceback = debug.get_traceback(0)
local s = string.format("%s:", msg)
for i=1,#traceback - 2 do
local frame = traceback[i]
s = s .. "\n\t"..tb_frame_tostring(frame)
end
return s
end)
if err then
error(err)
end
end)
local id = __vc_next_coroutine local id = __vc_next_coroutine
__vc_next_coroutine = __vc_next_coroutine + 1 __vc_next_coroutine = __vc_next_coroutine + 1
__vc_coroutines[id] = co __vc_coroutines[id] = co
@ -334,6 +351,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

@ -10,8 +10,9 @@ vc = {
is_client = function() is_client = function()
return not _vc_headless return not _vc_headless
end, end,
get_version = __vc_app.get_version,
get_setting = __vc_app.get_setting, get_setting = __vc_app.get_setting,
get_setting_str = __vc_app.get_setting_str, str_setting = __vc_app.str_setting,
get_setting_info = __vc_app.get_setting_info, get_setting_info = __vc_app.get_setting_info,
} }

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

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

@ -16,6 +16,9 @@ inline constexpr bool ENGINE_DEBUG_BUILD = true;
inline const std::string ENGINE_VERSION_STRING = "0.32"; inline const std::string ENGINE_VERSION_STRING = "0.32";
inline constexpr uint MAX_SUBPROCESSES = 1;
inline constexpr uint MAX_SUBPROCESS_DEPTH = 2;
/// @brief world regions format version /// @brief world regions format version
inline constexpr uint REGION_FORMAT_VERSION = 3; inline constexpr uint REGION_FORMAT_VERSION = 3;

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

@ -26,17 +26,21 @@ std::unique_ptr<Content> ContentBuilder::build() {
def.rt.tags.insert(tags.add(tag)); def.rt.tags.insert(tags.add(tag));
} }
auto isSolid = [](const Variant& variant) {
return variant.model.type == BlockModelType::BLOCK ||
variant.culling == CullingMode::OPTIONAL;
};
if (def.variants) { if (def.variants) {
for (auto& variant : def.variants->variants) { for (auto& variant : def.variants->variants) {
variant.rt.solid = variant.rt.solid = isSolid(variant) || def.explictlySolid;
variant.model.type == BlockModelType::BLOCK ||
def.explictlySolid;
} }
def.defaults = def.variants->variants.at(0); def.defaults = def.variants->variants.at(0);
} else { } else {
def.defaults.rt.solid = def.defaults.rt.solid = isSolid(def.defaults) || def.explictlySolid;
def.defaults.model.type == BlockModelType::BLOCK || }
def.explictlySolid; if (def.material.empty()) {
defaults.at("block-material").get(def.material);
} }
const float EPSILON = 0.01f; const float EPSILON = 0.01f;

View file

@ -506,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();
@ -516,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,
@ -524,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

@ -89,6 +89,7 @@ template<> void ContentUnitLoader<EntityDef>::loadUnit(
root.at("skeleton-name").get(def.skeletonName); root.at("skeleton-name").get(def.skeletonName);
root.at("material").get(def.material); root.at("material").get(def.material);
root.at("blocking").get(def.blocking); root.at("blocking").get(def.blocking);
root.at("selectable").get(def.selectable);
root.at("solid").get(def.solid); root.at("solid").get(def.solid);
root.at("mass").get(def.mass); root.at("mass").get(def.mass);
root.at("elasticity").get(def.elasticity); root.at("elasticity").get(def.elasticity);

View file

@ -9,6 +9,10 @@
#include <stdexcept> #include <stdexcept>
#include <unordered_map> #include <unordered_map>
#ifdef VC_ENABLE_REFLECTION
#include "util/EnumMetadata.hpp"
#endif
namespace util { namespace util {
template<class T> class Buffer; template<class T> class Buffer;
} }
@ -70,6 +74,14 @@ namespace dv {
return ptr != nullptr; return ptr != nullptr;
} }
inline value* operator->() noexcept {
return ptr;
}
inline const value* operator->() const noexcept {
return ptr;
}
inline value& operator*() noexcept { inline value& operator*() noexcept {
return *ptr; return *ptr;
} }
@ -78,6 +90,11 @@ namespace dv {
return *ptr; return *ptr;
} }
#ifdef VC_ENABLE_REFLECTION
template <typename T>
bool get(T& dst, const util::EnumMetadata<T>& mt) const;
#endif
bool get(std::string& dst) const; bool get(std::string& dst) const;
bool get(bool& dst) const; bool get(bool& dst) const;
bool get(char& dst) const; bool get(char& dst) const;
@ -552,6 +569,17 @@ namespace dv {
} }
return false; return false;
} }
#ifdef VC_ENABLE_REFLECTION
template <typename T>
inline bool optionalvalue::get(T& dst, const util::EnumMetadata<T>& mt) const {
if (ptr) {
return mt.getItem(ptr->asString(), dst);
}
return false;
}
#endif
inline bool optionalvalue::get(std::string& dst) const { inline bool optionalvalue::get(std::string& dst) const {
if (ptr) { if (ptr) {
dst = ptr->asString(); dst = ptr->asString();

View file

@ -0,0 +1,59 @@
#include "AppScriptsControl.hpp"
#include "debug/Logger.hpp"
#include "engine/CoreParameters.hpp"
#include "io/io.hpp"
#include "Project.hpp"
#include "logic/scripting/scripting.hpp"
static debug::Logger logger("app-scripts");
AppScriptsControl::AppScriptsControl(const CoreParameters& params) {
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,29 @@
#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);
void tick();
void loadProjectClientScript();
void terminate(std::string_view reason);
void onScreenChange(const std::string& name, bool show);
bool isFinished() const;
private:
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,22 +1,19 @@
#pragma once #pragma once
#include "interfaces/Serializable.hpp"
#include "io/path.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";
static inline std::string RECORD_AUDIO = "record-audio"; static inline std::string RECORD_AUDIO = "record-audio";
static inline std::string WRITE_TO_USER = "write-to-user"; static inline std::string WRITE_TO_USER = "write-to-user";
static inline std::string SUB_INSTANCES = "sub-instances";
std::set<std::string> permissions; std::set<std::string> permissions;
@ -26,16 +23,12 @@ struct Permissions {
struct Project : Serializable { struct Project : Serializable {
std::string name; std::string name;
std::string title; std::string title;
io::path path;
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

@ -12,7 +12,9 @@ struct CoreParameters {
std::filesystem::path userFolder = "."; std::filesystem::path userFolder = ".";
std::filesystem::path scriptFile; std::filesystem::path scriptFile;
std::filesystem::path projectFolder; std::filesystem::path projectFolder;
std::filesystem::path logFile;
std::string debugServerString; std::string debugServerString;
int tps = 20; int tps = 20;
int subProcessDepth = 0;
std::unordered_map<std::string, std::string> projectArgs; std::unordered_map<std::string, std::string> projectArgs;
}; };

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);
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) {
@ -382,13 +380,14 @@ void Engine::loadAssets() {
void Engine::loadProject() { void Engine::loadProject() {
io::path projectFile = "project:project.toml"; io::path projectFile = "project:project.toml";
project = std::make_unique<Project>(); project = std::make_unique<Project>();
project->path = "project:";
project->deserialize(io::read_object(projectFile)); project->deserialize(io::read_object(projectFile));
logger.info() << "loaded project " << util::quote(project->name); logger.info() << "loaded project " << util::quote(project->name);
} }
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 +396,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();
} }
} }
@ -414,6 +413,7 @@ void Engine::onWorldClosed() {
} }
void Engine::quit() { void Engine::quit() {
logger.info() << "quitSignal set to true";
quitSignal = true; quitSignal = true;
if (!isHeadless()) { if (!isHeadless()) {
window->setShouldClose(true); window->setShouldClose(true);

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) {
@ -36,8 +40,10 @@ 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() && !engine.isQuitSignal()) {
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

@ -30,6 +30,7 @@ void ContentGfxCache::refreshVariant(
uint8_t variantIndex, uint8_t variantIndex,
const Atlas& atlas const Atlas& atlas
) { ) {
std::unordered_map<std::string, std::string> optionalTextures;
bool denseRender = settings.denseRender.get(); bool denseRender = settings.denseRender.get();
for (uint side = 0; side < 6; side++) { for (uint side = 0; side < 6; side++) {
std::string tex = variant.textureFaces[side]; std::string tex = variant.textureFaces[side];
@ -43,6 +44,7 @@ void ContentGfxCache::refreshVariant(
texOpaque = tex; texOpaque = tex;
} else if (variant.culling == CullingMode::OPTIONAL && !denseRender) { } else if (variant.culling == CullingMode::OPTIONAL && !denseRender) {
tex = texOpaque; tex = texOpaque;
optionalTextures[variant.textureFaces[side]] = texOpaque;
} }
size_t index = getRegionIndex(def.rt.id, variantIndex, side, false); size_t index = getRegionIndex(def.rt.id, variantIndex, side, false);
sideregions[index] = atlas.get(tex); sideregions[index] = atlas.get(tex);
@ -56,7 +58,12 @@ void ContentGfxCache::refreshVariant(
if (pos == std::string::npos) { if (pos == std::string::npos) {
continue; continue;
} }
if (auto region = atlas.getIf(mesh.texture.substr(pos+1))) { std::string texture = mesh.texture.substr(pos+1);
const auto& found = optionalTextures.find(texture);
if (found != optionalTextures.end()) {
texture = found->second;
}
if (auto region = atlas.getIf(texture)) {
for (auto& vertex : mesh.vertices) { for (auto& vertex : mesh.vertices) {
vertex.uv = region->apply(vertex.uv); vertex.uv = region->apply(vertex.uv);
} }

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

@ -4,7 +4,7 @@
#include <GL/glew.h> #include <GL/glew.h>
Cubemap::Cubemap(uint width, uint height, ImageFormat imageFormat) Cubemap::Cubemap(uint width, uint height, ImageFormat imageFormat)
: Texture(0, width, height) : Texture(0U, width, height, imageFormat)
{ {
glGenTextures(1, &id); glGenTextures(1, &id);
glBindTexture(GL_TEXTURE_CUBE_MAP, id); glBindTexture(GL_TEXTURE_CUBE_MAP, id);
@ -14,7 +14,6 @@ Cubemap::Cubemap(uint width, uint height, ImageFormat imageFormat)
glTexParameterf(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameterf(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); glTexParameterf(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
uint format = gl::to_glenum(imageFormat);
for (uint face = 0; face < 6; face++) { for (uint face = 0; face < 6; face++) {
glTexImage2D( glTexImage2D(
GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, GL_TEXTURE_CUBE_MAP_POSITIVE_X + face,
@ -37,3 +36,23 @@ void Cubemap::bind() const {
void Cubemap::unbind() const { void Cubemap::unbind() const {
glBindTexture(GL_TEXTURE_CUBE_MAP, 0); glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
} }
void Cubemap::reload(const ubyte* data, uint width, uint height) {
this->width = width;
this->height = height;
glBindTexture(GL_TEXTURE_CUBE_MAP, id);
for (uint face = 0; face < 6; face++) {
glTexImage2D(
GL_TEXTURE_CUBE_MAP_POSITIVE_X + face,
0,
format,
width,
height,
0,
format,
GL_UNSIGNED_BYTE,
data
);
}
glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
}

View file

@ -9,4 +9,5 @@ public:
virtual void bind() const override; virtual void bind() const override;
virtual void unbind() const override; virtual void unbind() const override;
virtual void reload(const ubyte* data, uint w, uint h) override;
}; };

View file

@ -74,6 +74,18 @@ DrawContext::~DrawContext() {
if (lineWidth != parent->lineWidth) { if (lineWidth != parent->lineWidth) {
glLineWidth(parent->lineWidth); glLineWidth(parent->lineWidth);
} }
for (int i = 0; i < textures.size(); i++) {
auto texture = parent->textures[i];
if (textures[i] != texture) {
glActiveTexture(GL_TEXTURE0 + i);
if (texture == nullptr) {
glBindTexture(GL_TEXTURE_2D, 0);
} else {
texture->bind();
}
}
}
} }
const glm::uvec2& DrawContext::getViewport() const { const glm::uvec2& DrawContext::getViewport() const {
@ -154,3 +166,13 @@ void DrawContext::setLineWidth(float width) {
lineWidth = width; lineWidth = width;
glLineWidth(width); glLineWidth(width);
} }
void DrawContext::useTexture(int target, const Bindable* texture) {
glActiveTexture(GL_TEXTURE0 + target);
if (texture == nullptr) {
glBindTexture(GL_TEXTURE_2D, 0);
} else {
texture->bind();
}
textures.at(target) = texture;
}

View file

@ -1,5 +1,6 @@
#pragma once #pragma once
#include <array>
#include <glm/vec2.hpp> #include <glm/vec2.hpp>
#include <glm/vec4.hpp> #include <glm/vec4.hpp>
@ -23,6 +24,7 @@ class DrawContext {
BlendMode blendMode = BlendMode::normal; BlendMode blendMode = BlendMode::normal;
int scissorsCount = 0; int scissorsCount = 0;
float lineWidth = 1.0f; float lineWidth = 1.0f;
std::array<const Bindable*, advanced_pipeline::TARGETS_COUNT> textures {};
public: public:
DrawContext( DrawContext(
const DrawContext* parent, const DrawContext* parent,
@ -44,4 +46,5 @@ public:
void setBlendMode(BlendMode mode); void setBlendMode(BlendMode mode);
void setScissors(const glm::vec4& area); void setScissors(const glm::vec4& area);
void setLineWidth(float width); void setLineWidth(float width);
void useTexture(int target, const Bindable* texture);
}; };

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

@ -1,14 +1,14 @@
#include "Framebuffer.hpp" #include "Framebuffer.hpp"
#include <GL/glew.h> #include <GL/glew.h>
#include "Texture.hpp" #include "Texture.hpp"
#include "debug/Logger.hpp" #include "debug/Logger.hpp"
static debug::Logger logger("gl-framebuffer"); static debug::Logger logger("gl-framebuffer");
Framebuffer::Framebuffer(uint fbo, uint depth, std::unique_ptr<Texture> texture) Framebuffer::Framebuffer(uint fbo, uint depth, std::unique_ptr<Texture> texture)
: fbo(fbo), depth(depth), texture(std::move(texture)) : fbo(fbo), depth(depth), texture(std::move(texture)) {
{
if (this->texture) { if (this->texture) {
width = this->texture->getWidth(); width = this->texture->getWidth();
height = this->texture->getHeight(); height = this->texture->getHeight();
@ -18,35 +18,55 @@ Framebuffer::Framebuffer(uint fbo, uint depth, std::unique_ptr<Texture> texture)
} }
} }
static std::unique_ptr<Texture> create_texture(int width, int height, int format) { static std::unique_ptr<Texture> create_texture(
GLuint tex; int width, int height, bool alpha
glGenTextures(1, &tex); ) {
glBindTexture(GL_TEXTURE_2D, tex); GLenum glformat = alpha ? GL_RGBA : GL_RGB;
glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, nullptr);
GLuint textureid;
glGenTextures(1, &textureid);
glBindTexture(GL_TEXTURE_2D, textureid);
glTexImage2D(
GL_TEXTURE_2D,
0,
glformat,
width,
height,
0,
glformat,
GL_UNSIGNED_BYTE,
nullptr
);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0); glFramebufferTexture2D(
return std::make_unique<Texture>(tex, width, height); GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textureid, 0
);
return std::make_unique<Texture>(
textureid,
width,
height,
alpha ? ImageFormat::RGBA8888 : ImageFormat::RGB888
);
} }
Framebuffer::Framebuffer(uint width, uint height, bool alpha) Framebuffer::Framebuffer(uint width, uint height, bool alpha)
: width(width), height(height) : width(width), height(height) {
{
glGenFramebuffers(1, &fbo); glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_FRAMEBUFFER, fbo); glBindFramebuffer(GL_FRAMEBUFFER, fbo);
format = alpha ? GL_RGBA : GL_RGB;
// Setup color attachment (texture) // Setup color attachment (texture)
texture = create_texture(width, height, format); texture = create_texture(width, height, alpha);
// Setup depth attachment // Setup depth attachment
glGenRenderbuffers(1, &depth); glGenRenderbuffers(1, &depth);
glBindRenderbuffer(GL_RENDERBUFFER, depth); glBindRenderbuffer(GL_RENDERBUFFER, depth);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, width, height); glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, width, height);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depth); glFramebufferRenderbuffer(
GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depth
);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
logger.error() << "framebuffer is not complete!"; logger.error() << "framebuffer is not complete!";
@ -60,11 +80,15 @@ Framebuffer::~Framebuffer() {
glDeleteRenderbuffers(1, &depth); glDeleteRenderbuffers(1, &depth);
} }
void Framebuffer::bind() { void Framebuffer::setTexture(std::unique_ptr<Texture> texture) {
this->texture = std::move(texture);
}
void Framebuffer::bind() const {
glBindFramebuffer(GL_FRAMEBUFFER, fbo); glBindFramebuffer(GL_FRAMEBUFFER, fbo);
} }
void Framebuffer::unbind() { void Framebuffer::unbind() const {
glBindFramebuffer(GL_FRAMEBUFFER, 0); glBindFramebuffer(GL_FRAMEBUFFER, 0);
} }
@ -81,7 +105,7 @@ void Framebuffer::resize(uint width, uint height) {
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, width, height); glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, width, height);
glBindRenderbuffer(GL_RENDERBUFFER, 0); glBindRenderbuffer(GL_RENDERBUFFER, 0);
texture = create_texture(width, height, format); texture->resize(width, height);
glBindFramebuffer(GL_FRAMEBUFFER, 0); glBindFramebuffer(GL_FRAMEBUFFER, 0);
} }

View file

@ -12,18 +12,19 @@ class Framebuffer : public Bindable {
uint depth; uint depth;
uint width; uint width;
uint height; uint height;
uint format;
std::shared_ptr<Texture> texture; std::shared_ptr<Texture> texture;
public: public:
Framebuffer(uint fbo, uint depth, std::unique_ptr<Texture> texture); Framebuffer(uint fbo, uint depth, std::unique_ptr<Texture> texture);
Framebuffer(uint width, uint height, bool alpha=false); Framebuffer(uint width, uint height, bool alpha=false);
~Framebuffer(); ~Framebuffer();
void setTexture(std::unique_ptr<Texture> texture);
/// @brief Use framebuffer /// @brief Use framebuffer
void bind() override; void bind() const override;
/// @brief Stop using framebuffer /// @brief Stop using framebuffer
void unbind() override; void unbind() const override;
/// @brief Update framebuffer texture size /// @brief Update framebuffer texture size
/// @param width new width /// @param width new width

View file

@ -108,7 +108,7 @@ GBuffer::~GBuffer() {
glDeleteFramebuffers(1, &ssaoFbo); glDeleteFramebuffers(1, &ssaoFbo);
} }
void GBuffer::bind() { void GBuffer::bind() const {
glBindFramebuffer(GL_FRAMEBUFFER, fbo); glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glClear(GL_COLOR_BUFFER_BIT); glClear(GL_COLOR_BUFFER_BIT);
} }
@ -117,7 +117,7 @@ void GBuffer::bindSSAO() const {
glBindFramebuffer(GL_FRAMEBUFFER, ssaoFbo); glBindFramebuffer(GL_FRAMEBUFFER, ssaoFbo);
} }
void GBuffer::unbind() { void GBuffer::unbind() const {
glBindFramebuffer(GL_FRAMEBUFFER, 0); glBindFramebuffer(GL_FRAMEBUFFER, 0);
} }

View file

@ -9,9 +9,9 @@ public:
GBuffer(uint width, uint height); GBuffer(uint width, uint height);
~GBuffer() override; ~GBuffer() override;
void bind() override; void bind() const override;
void bindSSAO() const; void bindSSAO() const;
void unbind() override; void unbind() const override;
void bindBuffers() const; void bindBuffers() const;
void bindSSAOBuffer() const; void bindSSAOBuffer() const;

View file

@ -7,8 +7,8 @@
uint Texture::MAX_RESOLUTION = 1024; // Window.initialize overrides it uint Texture::MAX_RESOLUTION = 1024; // Window.initialize overrides it
Texture::Texture(uint id, uint width, uint height) Texture::Texture(uint id, uint width, uint height, ImageFormat imageFormat)
: id(id), width(width), height(height) { : id(id), width(width), height(height), format(gl::to_glenum(imageFormat)) {
} }
Texture::Texture(const ubyte* data, uint width, uint height, ImageFormat imageFormat) Texture::Texture(const ubyte* data, uint width, uint height, ImageFormat imageFormat)
@ -17,7 +17,7 @@ Texture::Texture(const ubyte* data, uint width, uint height, ImageFormat imageFo
glBindTexture(GL_TEXTURE_2D, id); glBindTexture(GL_TEXTURE_2D, id);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1); glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
GLenum format = gl::to_glenum(imageFormat); format = gl::to_glenum(imageFormat);
glTexImage2D( glTexImage2D(
GL_TEXTURE_2D, 0, format, width, height, 0, GL_TEXTURE_2D, 0, format, width, height, 0,
format, GL_UNSIGNED_BYTE, static_cast<const GLvoid*>(data) format, GL_UNSIGNED_BYTE, static_cast<const GLvoid*>(data)
@ -41,23 +41,27 @@ void Texture::unbind() const {
glBindTexture(GL_TEXTURE_2D, 0); glBindTexture(GL_TEXTURE_2D, 0);
} }
void Texture::resize(uint width, uint height) {
reload(nullptr, width, height);
}
void Texture::reload(const ImageData& image) { void Texture::reload(const ImageData& image) {
width = image.getWidth();
height = image.getHeight();
reload(image.getData(), width, height); reload(image.getData(), width, height);
} }
void Texture::reload(const ubyte* data, uint width, uint height) { void Texture::reload(const ubyte* data, uint width, uint height) {
this->width = width;
this->height = height;
glBindTexture(GL_TEXTURE_2D, id); glBindTexture(GL_TEXTURE_2D, id);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0,
GL_RGBA, GL_UNSIGNED_BYTE, static_cast<const GLvoid*>(data)); format, GL_UNSIGNED_BYTE, static_cast<const GLvoid*>(data));
glGenerateMipmap(GL_TEXTURE_2D); glGenerateMipmap(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, 0); glBindTexture(GL_TEXTURE_2D, 0);
} }
void Texture::reloadPartial(const ImageData& image, uint x, uint y, uint w, uint h) { void Texture::reloadPartial(const ImageData& image, uint x, uint y, uint w, uint h) {
glBindTexture(GL_TEXTURE_2D, id); glBindTexture(GL_TEXTURE_2D, id);
glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, w, h, GL_RGBA, GL_UNSIGNED_BYTE, image.getData()); glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, w, h, format, GL_UNSIGNED_BYTE, image.getData());
glGenerateMipmap(GL_TEXTURE_2D); glGenerateMipmap(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, 0); glBindTexture(GL_TEXTURE_2D, 0);
} }

View file

@ -1,29 +1,32 @@
#pragma once #pragma once
#include "typedefs.hpp"
#include "maths/UVRegion.hpp"
#include "ImageData.hpp"
#include <memory> #include <memory>
class Texture { #include "ImageData.hpp"
#include "commons.hpp"
#include "maths/UVRegion.hpp"
#include "typedefs.hpp"
class Texture : public Bindable {
protected: protected:
uint id; uint id;
uint width; uint width;
uint height; uint height;
uint format;
public: public:
Texture(uint id, uint width, uint height); Texture(uint id, uint width, uint height, ImageFormat imageFormat);
Texture(const ubyte* data, uint width, uint height, ImageFormat format); Texture(const ubyte* data, uint width, uint height, ImageFormat format);
virtual ~Texture(); virtual ~Texture();
virtual void bind() const; virtual void bind() const override;
virtual void unbind() const; virtual void unbind() const override;
void reload(const ubyte* data, uint w, uint h); virtual void reload(const ubyte* data, uint w, uint h);
void reloadPartial(const ImageData& image, uint x, uint y, uint w, uint h); void reloadPartial(const ImageData& image, uint x, uint y, uint w, uint h);
void setNearestFilter(); void setNearestFilter();
void reload(const ImageData& image); void reload(const ImageData& image);
virtual void resize(uint w, uint h);
void setMipMapping(bool flag, bool pixelated); void setMipMapping(bool flag, bool pixelated);

View file

@ -59,6 +59,8 @@ namespace advanced_pipeline {
inline constexpr int TARGET_SSAO = 5; inline constexpr int TARGET_SSAO = 5;
inline constexpr int TARGET_SHADOWS0 = 6; inline constexpr int TARGET_SHADOWS0 = 6;
inline constexpr int TARGET_SHADOWS1 = 7; inline constexpr int TARGET_SHADOWS1 = 7;
inline constexpr int TARGETS_COUNT = 8;
} }
VC_ENUM_METADATA(CursorShape) VC_ENUM_METADATA(CursorShape)
@ -85,6 +87,6 @@ class Bindable {
public: public:
virtual ~Bindable() = default; virtual ~Bindable() = default;
virtual void bind() = 0; virtual void bind() const = 0;
virtual void unbind() = 0; virtual void unbind() const = 0;
}; };

View file

@ -538,7 +538,6 @@ void BlocksRenderer::render(
break; break;
} }
case BlockModelType::CUSTOM: { case BlockModelType::CUSTOM: {
if (!denseRender)
blockCustomModel( blockCustomModel(
{x, y, z}, {x, y, z},
def, def,

View file

@ -34,4 +34,7 @@ void HandsRenderer::render(const Camera& camera) {
glm::vec3(), glm::vec3(),
glm::vec3(1.0f) glm::vec3(1.0f)
); );
modelBatch.render();
modelBatch.setLightsOffset(glm::vec3());
} }

View file

@ -1,91 +1,64 @@
#include "Skybox.hpp" #include "Skybox.hpp"
#include "assets/Assets.hpp"
#include "graphics/core/Shader.hpp" #include <GL/glew.h>
#include "graphics/core/Mesh.hpp"
#include "graphics/core/Batch3D.hpp"
#include "graphics/core/Texture.hpp"
#include "graphics/core/Cubemap.hpp"
#include "graphics/core/Framebuffer.hpp"
#include "graphics/core/DrawContext.hpp"
#include "window/Window.hpp"
#include "window/Camera.hpp"
#include "maths/UVRegion.hpp"
#include <cmath> #include <cmath>
#include <iostream>
#include <GL/glew.h>
#include <glm/glm.hpp> #include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/constants.hpp> #include <glm/gtc/constants.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <limits>
#include "assets/Assets.hpp"
#include "graphics/core/Batch3D.hpp"
#include "graphics/core/Cubemap.hpp"
#include "graphics/core/DrawContext.hpp"
#include "graphics/core/Framebuffer.hpp"
#include "graphics/core/Mesh.hpp"
#include "graphics/core/Shader.hpp"
#include "graphics/core/Texture.hpp"
#include "maths/UVRegion.hpp"
#include "window/Camera.hpp"
#include "window/Window.hpp"
using namespace advanced_pipeline; using namespace advanced_pipeline;
const int STARS_COUNT = 3000; const int STARS_COUNT = 3000;
const int STARS_SEED = 632; const int STARS_SEED = 632;
Skybox::Skybox(uint size, Shader& shader) Skybox::Skybox(uint size, const Assets& assets)
: size(size), : size(size),
shader(shader), assets(assets),
batch3d(std::make_unique<Batch3D>(4096)) shader(assets.require<Shader>("skybox_gen")),
{ batch3d(std::make_unique<Batch3D>(4096)) {
auto cubemap = std::make_unique<Cubemap>(size, size, ImageFormat::RGB888);
uint fboid; SkyboxVertex vertices[] {
glGenFramebuffers(1, &fboid);
fbo = std::make_unique<Framebuffer>(fboid, 0, std::move(cubemap));
SkyboxVertex vertices[]{
{{-1.0f, -1.0f}}, {{-1.0f, -1.0f}},
{{-1.0f, 1.0f}}, {{-1.0f, 1.0f}},
{{1.0f, 1.0f}}, {{1.0f, 1.0f}},
{{-1.0f, -1.0f}}, {{-1.0f, -1.0f}},
{{1.0f, 1.0f}}, {{1.0f, 1.0f}},
{{1.0f, -1.0f}} {{1.0f, -1.0f}}};
};
mesh = std::make_unique<Mesh<SkyboxVertex>>(vertices, 6); mesh = std::make_unique<Mesh<SkyboxVertex>>(vertices, 6);
setMode(mode);
sprites.push_back(SkySprite {
"misc/moon",
glm::pi<float>() * 0.5f,
4.0f,
false,
glm::pi<float>() * 0.25f,
});
sprites.push_back(SkySprite {
"misc/moon_flare",
glm::pi<float>() * 0.5f,
0.5f,
false,
glm::pi<float>() * 0.25f,
});
sprites.push_back(SkySprite {
"misc/sun",
glm::pi<float>() * 1.5f,
4.0f,
true,
glm::pi<float>() * 0.25f,
});
} }
Skybox::~Skybox() = default; Skybox::~Skybox() = default;
void Skybox::drawBackground( void Skybox::drawBackground(const Camera& camera, int width, int height) {
const Camera& camera, const Assets& assets, int width, int height
) {
auto backShader = assets.get<Shader>("background"); auto backShader = assets.get<Shader>("background");
backShader->use(); backShader->use();
backShader->uniformMatrix("u_view", camera.getView(false)); backShader->uniformMatrix("u_view", camera.getView(false));
backShader->uniform1f( backShader->uniform1f(
"u_zoom", camera.zoom * camera.getFov() / glm::half_pi<float>() "u_zoom", camera.zoom * camera.getFov() / glm::half_pi<float>()
); );
backShader->uniform1f("u_ar", float(width)/float(height)); backShader->uniform1f("u_ar", float(width) / float(height));
backShader->uniform1i("u_skybox", 1); backShader->uniform1i("u_skybox", 1);
bind();
auto texture = fbo->getTexture();
texture->bind();
mesh->draw(); mesh->draw();
unbind(); texture->unbind();
} }
void Skybox::drawStars(float angle, float opacity) { void Skybox::drawStars(float angle, float opacity) {
@ -93,9 +66,7 @@ void Skybox::drawStars(float angle, float opacity) {
random.setSeed(STARS_SEED); random.setSeed(STARS_SEED);
glm::mat4 rotation = glm::rotate( glm::mat4 rotation = glm::rotate(
glm::mat4(1.0f), glm::mat4(1.0f), -angle + glm::pi<float>() * 0.5f, glm::vec3(0, 0, -1)
-angle + glm::pi<float>() * 0.5f,
glm::vec3(0, 0, -1)
); );
rotation = glm::rotate(rotation, sunAltitude, glm::vec3(1, 0, 0)); rotation = glm::rotate(rotation, sunAltitude, glm::vec3(1, 0, 0));
@ -108,41 +79,21 @@ void Skybox::drawStars(float angle, float opacity) {
glm::vec3 pos = glm::vec4(rx, ry, rz, 1) * rotation; glm::vec3 pos = glm::vec4(rx, ry, rz, 1) * rotation;
float sopacity = random.randFloat(); float sopacity = random.randFloat();
if (pos.y < 0.0f) if (pos.y < 0.0f) continue;
continue;
sopacity *= (0.2f + std::sqrt(std::cos(angle)) * 0.5f) - 0.05f; sopacity *= (0.2f + std::sqrt(std::cos(angle)) * 0.5f) - 0.05f;
glm::vec4 tint (1,1,1, sopacity * opacity); glm::vec4 tint(1, 1, 1, sopacity * opacity);
batch3d->point(pos * depth, tint); batch3d->point(pos * depth, tint);
} }
batch3d->flushPoints(); batch3d->flushPoints();
} }
void Skybox::draw( void Skybox::drawSkySprites(
const DrawContext& pctx,
const Camera& camera,
const Assets& assets,
float daytime, float daytime,
float fog) float angle,
{ float opacity,
const glm::uvec2& viewport = pctx.getViewport(); const std::vector<SkySprite>& sprites
) {
glActiveTexture(GL_TEXTURE0);
drawBackground(camera, assets, viewport.x, viewport.y);
DrawContext ctx = pctx.sub();
ctx.setBlendMode(BlendMode::addition);
auto shader = assets.get<Shader>("ui3d");
shader->use();
shader->uniformMatrix("u_projview", camera.getProjView(false));
shader->uniformMatrix("u_apply", glm::mat4(1.0f));
batch3d->begin();
float angle = daytime * glm::pi<float>() * 2.0f;
float opacity = glm::pow(1.0f - fog, 7.0f);
float depthScale = 2e3; float depthScale = 2e3;
for (auto& sprite : sprites) { for (auto& sprite : sprites) {
batch3d->texture(assets.get<Texture>(sprite.texture)); batch3d->texture(assets.get<Texture>(sprite.texture));
@ -159,39 +110,124 @@ void Skybox::draw(
glm::vec3 pos = glm::vec4(0, distance, 0, 1) * rotation; glm::vec3 pos = glm::vec4(0, distance, 0, 1) * rotation;
glm::vec3 up = glm::vec4(depthScale, 0, 0, 1) * rotation; glm::vec3 up = glm::vec4(depthScale, 0, 0, 1) * rotation;
glm::vec3 right = glm::vec4(0, 0, depthScale, 1) * rotation; glm::vec3 right = glm::vec4(0, 0, depthScale, 1) * rotation;
glm::vec4 tint (1,1,1, opacity); glm::vec4 tint(1, 1, 1, opacity);
if (!sprite.emissive) { if (!sprite.emissive) {
tint *= 0.6f + std::cos(angle)*0.4; tint *= 0.6f + std::cos(angle) * 0.4;
} }
batch3d->sprite(pos, right, batch3d->sprite(pos, right, up, 1, 1, UVRegion(), tint);
up, 1, 1, UVRegion(), tint);
} }
batch3d->flush(); batch3d->flush();
drawStars(angle, opacity);
} }
void Skybox::refresh(const DrawContext& pctx, float t, float mie, const glm::vec3& tint, const glm::vec3& hightlight, uint quality) { void Skybox::draw(
const Environment& environment,
const DrawContext& pctx,
const Camera& camera,
float daytime,
float fog
) {
const glm::uvec2& viewport = pctx.getViewport();
drawBackground(camera, viewport.x, viewport.y);
float angle = daytime * glm::pi<float>() * 2.0f;
float opacity = glm::pow(1.0f - fog, 7.0f);
DrawContext ctx = pctx.sub();
ctx.setBlendMode(BlendMode::addition);
if (environment.sky.sprites.empty() && !environment.sky.stars) {
return;
}
auto shader = assets.get<Shader>("ui3d");
shader->use();
shader->uniformMatrix("u_projview", camera.getProjView(false));
shader->uniformMatrix("u_apply", glm::mat4(1.0f));
batch3d->begin();
if (!environment.sky.sprites.empty()) {
drawSkySprites(daytime, angle, opacity, environment.sky.sprites);
}
if (environment.sky.stars) {
drawStars(angle, opacity);
}
}
void Skybox::setMode(SkyMode mode) {
if (this->mode == mode && fbo) {
return;
}
this->mode = mode;
uint size = mode == SkyMode::BOX ? this->size : 1U;
prevT = std::numeric_limits<float>().min();
uint fboid;
glGenFramebuffers(1, &fboid);
fbo = std::make_unique<Framebuffer>(
fboid,
false,
std::make_unique<Cubemap>(size, size, ImageFormat::RGB888)
);
}
void Skybox::refresh(
const Environment& environment,
const DrawContext& pctx,
float dayTime,
float mie,
const glm::vec3& tint,
const glm::vec3& hightlight,
uint quality
) {
setMode(environment.sky.mode);
if (mode == SkyMode::NONE) {
return;
} else if (mode == SkyMode::SOLID) {
DrawContext ctx = pctx.sub();
ctx.setDepthMask(false);
ctx.setDepthTest(false);
ctx.setFramebuffer(fbo.get());
ctx.setViewport({fbo->getWidth(), fbo->getHeight()});
ctx.useTexture(advanced_pipeline::TARGET_SKYBOX, fbo->getTexture());
ctx.useTexture(advanced_pipeline::TARGET_COLOR, nullptr);
auto cubemap = dynamic_cast<Cubemap*>(fbo->getTexture());
assert(cubemap != nullptr);
for (int face = 0; face < 6; face++) {
glFramebufferTexture2D(
GL_FRAMEBUFFER,
GL_COLOR_ATTACHMENT0,
GL_TEXTURE_CUBE_MAP_POSITIVE_X + face,
cubemap->getId(),
0
);
glClearColor(tint.r, tint.g, tint.b, 1);
glClear(GL_COLOR_BUFFER_BIT);
}
return;
}
frameid++; frameid++;
float dayTime = t;
DrawContext ctx = pctx.sub(); DrawContext ctx = pctx.sub();
ctx.setDepthMask(false); ctx.setDepthMask(false);
ctx.setDepthTest(false); ctx.setDepthTest(false);
ctx.setFramebuffer(fbo.get()); ctx.setFramebuffer(fbo.get());
ctx.setViewport({size, size}); ctx.setViewport({fbo->getWidth(), fbo->getHeight()});
ctx.useTexture(advanced_pipeline::TARGET_SKYBOX, fbo->getTexture());
ctx.useTexture(advanced_pipeline::TARGET_COLOR, nullptr);
auto cubemap = dynamic_cast<Cubemap*>(fbo->getTexture()); auto cubemap = dynamic_cast<Cubemap*>(fbo->getTexture());
assert(cubemap != nullptr); assert(cubemap != nullptr);
ready = true; float t = dayTime * glm::two_pi<float>();
glActiveTexture(GL_TEXTURE0 + TARGET_SKYBOX);
cubemap->bind();
shader.use();
t *= glm::two_pi<float>();
lightDir = glm::normalize(glm::vec3(sin(t), -cos(t), 0.0f)); lightDir = glm::normalize(glm::vec3(sin(t), -cos(t), 0.0f));
float sunAngle = glm::radians((t / glm::two_pi<float>() - 0.25f) * 360.0f); float sunAngle = glm::radians((dayTime - 0.25f) * 360.0f);
float x = -glm::cos(sunAngle + glm::pi<float>() * 0.5f) * glm::radians(sunAltitude); float x = -glm::cos(sunAngle + glm::pi<float>() * 0.5f) *
glm::radians(sunAltitude);
float y = sunAngle - glm::pi<float>() * 0.5f; float y = sunAngle - glm::pi<float>() * 0.5f;
float z = glm::radians(0.0f); float z = glm::radians(0.0f);
rotation = glm::rotate(glm::mat4(1.0f), y, glm::vec3(0, 1, 0)); rotation = glm::rotate(glm::mat4(1.0f), y, glm::vec3(0, 1, 0));
@ -199,6 +235,7 @@ void Skybox::refresh(const DrawContext& pctx, float t, float mie, const glm::vec
rotation = glm::rotate(rotation, z, glm::vec3(0, 0, 1)); rotation = glm::rotate(rotation, z, glm::vec3(0, 0, 1));
lightDir = glm::vec3(rotation * glm::vec4(0, 0, -1, 1)); lightDir = glm::vec3(rotation * glm::vec4(0, 0, -1, 1));
shader.use();
shader.uniform1i("u_quality", quality); shader.uniform1i("u_quality", quality);
shader.uniform1f("u_mie", mie); shader.uniform1f("u_mie", mie);
shader.uniform3f("u_tint", tint); shader.uniform3f("u_tint", tint);
@ -207,23 +244,22 @@ void Skybox::refresh(const DrawContext& pctx, float t, float mie, const glm::vec
shader.uniform3f("u_lightDir", lightDir); shader.uniform3f("u_lightDir", lightDir);
shader.uniform1f("u_dayTime", dayTime); shader.uniform1f("u_dayTime", dayTime);
if (glm::abs(mie-prevMie) + glm::abs(t-prevT) + glm::abs(prevHighlight.r - hightlight.r) >= 0.01) { if (glm::abs(mie - prevMie) + glm::abs(t - prevT) +
glm::abs(prevHighlight.r - hightlight.r) >=
0.01) {
for (uint face = 0; face < 6; face++) { for (uint face = 0; face < 6; face++) {
refreshFace(face, cubemap); refreshFace(face, *cubemap);
} }
} else { } else {
uint face = frameid % 6; uint face = frameid % 6;
refreshFace(face, cubemap); refreshFace(face, *cubemap);
} }
prevMie = mie; prevMie = mie;
prevT = t; prevT = t;
prevHighlight = hightlight; prevHighlight = hightlight;
cubemap->unbind();
glActiveTexture(GL_TEXTURE0);
} }
void Skybox::refreshFace(uint face, Cubemap* cubemap) { void Skybox::refreshFace(uint face, Cubemap& cubemap) {
const glm::vec3 xaxs[] = { const glm::vec3 xaxs[] = {
{0.0f, 0.0f, -1.0f}, {0.0f, 0.0f, -1.0f},
{0.0f, 0.0f, 1.0f}, {0.0f, 0.0f, 1.0f},
@ -256,7 +292,7 @@ void Skybox::refreshFace(uint face, Cubemap* cubemap) {
GL_FRAMEBUFFER, GL_FRAMEBUFFER,
GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT0,
GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, GL_TEXTURE_CUBE_MAP_POSITIVE_X + face,
cubemap->getId(), cubemap.getId(),
0 0
); );
shader.uniform3f("u_xaxis", xaxs[face]); shader.uniform3f("u_xaxis", xaxs[face]);
@ -265,14 +301,6 @@ void Skybox::refreshFace(uint face, Cubemap* cubemap) {
mesh->draw(); mesh->draw();
} }
void Skybox::bind() const { const Cubemap* Skybox::getCubemap() const {
glActiveTexture(GL_TEXTURE0 + TARGET_SKYBOX); return dynamic_cast<const Cubemap*>(fbo->getTexture());
fbo->getTexture()->bind();
glActiveTexture(GL_TEXTURE0);
}
void Skybox::unbind() const {
glActiveTexture(GL_TEXTURE0 + TARGET_SKYBOX);
fbo->getTexture()->unbind();
glActiveTexture(GL_TEXTURE0);
} }

View file

@ -1,14 +1,17 @@
#pragma once #pragma once
#include <glm/glm.hpp>
#include <memory> #include <memory>
#include <string> #include <string>
#include <vector> #include <vector>
#include <glm/glm.hpp>
#include "typedefs.hpp"
#include "maths/fastmaths.hpp"
#include "graphics/core/MeshData.hpp"
template<typename VertexStructure> class Mesh; #include "graphics/core/MeshData.hpp"
#include "maths/fastmaths.hpp"
#include "typedefs.hpp"
#include "world/Environment.hpp"
template <typename VertexStructure>
class Mesh;
class Shader; class Shader;
class Assets; class Assets;
class Camera; class Camera;
@ -21,29 +24,20 @@ struct SkyboxVertex {
glm::vec2 position; glm::vec2 position;
static constexpr VertexAttribute ATTRIBUTES[] { static constexpr VertexAttribute ATTRIBUTES[] {
{VertexAttribute::Type::FLOAT, false, 2}, {VertexAttribute::Type::FLOAT, false, 2}, {{}, 0}};
{{}, 0}};
};
struct SkySprite {
std::string texture;
float phase;
float distance;
bool emissive;
float altitude;
}; };
class Skybox { class Skybox {
std::unique_ptr<Framebuffer> fbo; SkyMode mode = SkyMode::SOLID;
uint size; uint size;
std::unique_ptr<Framebuffer> fbo;
const Assets& assets;
Shader& shader; Shader& shader;
bool ready = false;
FastRandom random; FastRandom random;
glm::vec3 lightDir; glm::vec3 lightDir;
std::unique_ptr<Mesh<SkyboxVertex>> mesh; std::unique_ptr<Mesh<SkyboxVertex>> mesh;
std::unique_ptr<Batch3D> batch3d; std::unique_ptr<Batch3D> batch3d;
std::vector<SkySprite> sprites;
int frameid = 0; int frameid = 0;
float prevMie = -1.0f; float prevMie = -1.0f;
@ -53,30 +47,41 @@ class Skybox {
glm::mat4 rotation; glm::mat4 rotation;
void drawStars(float angle, float opacity); void drawStars(float angle, float opacity);
void drawBackground( void drawBackground(const Camera& camera, int width, int height);
const Camera& camera, const Assets& assets, int width, int height void drawSkySprites(
float daytime,
float angle,
float opacity,
const std::vector<SkySprite>& sprites
); );
void refreshFace(uint face, Cubemap* cubemap); void refreshFace(uint face, Cubemap& cubemap);
public: public:
Skybox(uint size, Shader& shader); Skybox(uint size, const Assets& assets);
~Skybox(); ~Skybox();
void setMode(SkyMode mode);
void draw( void draw(
const Environment& environment,
const DrawContext& pctx, const DrawContext& pctx,
const Camera& camera, const Camera& camera,
const Assets& assets,
float daytime, float daytime,
float fog float fog
); );
void refresh(const DrawContext& pctx, float t, float mie, const glm::vec3& tint, const glm::vec3& hightlight, uint quality); void refresh(
void bind() const; const Environment& environment,
void unbind() const; const DrawContext& pctx,
bool isReady() const { float t,
return ready; float mie,
} const glm::vec3& tint,
const glm::vec3& hightlight,
uint quality
);
const glm::vec3 getLightDir() const { const Cubemap* getCubemap() const;
const glm::vec3& getLightDir() const {
return lightDir; return lightDir;
} }
}; };

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

@ -1,12 +1,39 @@
#include "WorldRenderer.hpp" #include "WorldRenderer.hpp"
#include "BlockWrapsRenderer.hpp"
#include "ChunksRenderer.hpp"
#include "CloudsRenderer.hpp"
#include "DebugLinesRenderer.hpp"
#include "Emitter.hpp"
#include "HandsRenderer.hpp"
#include "ModelBatch.hpp"
#include "NamedSkeletons.hpp"
#include "ParticlesRenderer.hpp"
#include "PrecipitationRenderer.hpp"
#include "Skybox.hpp"
#include "TextNote.hpp"
#include "TextsRenderer.hpp"
#include "assets/Assets.hpp" #include "assets/Assets.hpp"
#include "assets/assets_util.hpp" #include "assets/assets_util.hpp"
#include "coders/GLSLExtension.hpp"
#include "content/Content.hpp" #include "content/Content.hpp"
#include "engine/Engine.hpp" #include "engine/Engine.hpp"
#include "coders/GLSLExtension.hpp"
#include "frontend/LevelFrontend.hpp"
#include "frontend/ContentGfxCache.hpp" #include "frontend/ContentGfxCache.hpp"
#include "frontend/LevelFrontend.hpp"
#include "graphics/commons/Model.hpp"
#include "graphics/core/Atlas.hpp"
#include "graphics/core/Batch3D.hpp"
#include "graphics/core/DrawContext.hpp"
#include "graphics/core/Font.hpp"
#include "graphics/core/Framebuffer.hpp"
#include "graphics/core/GBuffer.hpp"
#include "graphics/core/LineBatch.hpp"
#include "graphics/core/Mesh.hpp"
#include "graphics/core/PostProcessing.hpp"
#include "graphics/core/Shader.hpp"
#include "graphics/core/Shadows.hpp"
#include "graphics/core/Cubemap.hpp"
#include "graphics/core/Texture.hpp"
#include "items/Inventory.hpp" #include "items/Inventory.hpp"
#include "items/ItemDef.hpp" #include "items/ItemDef.hpp"
#include "items/ItemStack.hpp" #include "items/ItemStack.hpp"
@ -25,32 +52,6 @@
#include "world/Level.hpp" #include "world/Level.hpp"
#include "world/LevelEvents.hpp" #include "world/LevelEvents.hpp"
#include "world/World.hpp" #include "world/World.hpp"
#include "graphics/commons/Model.hpp"
#include "graphics/core/Atlas.hpp"
#include "graphics/core/Batch3D.hpp"
#include "graphics/core/DrawContext.hpp"
#include "graphics/core/LineBatch.hpp"
#include "graphics/core/Mesh.hpp"
#include "graphics/core/PostProcessing.hpp"
#include "graphics/core/Framebuffer.hpp"
#include "graphics/core/Shader.hpp"
#include "graphics/core/Texture.hpp"
#include "graphics/core/Font.hpp"
#include "graphics/core/Shadows.hpp"
#include "graphics/core/GBuffer.hpp"
#include "BlockWrapsRenderer.hpp"
#include "ParticlesRenderer.hpp"
#include "PrecipitationRenderer.hpp"
#include "HandsRenderer.hpp"
#include "NamedSkeletons.hpp"
#include "TextsRenderer.hpp"
#include "ChunksRenderer.hpp"
#include "DebugLinesRenderer.hpp"
#include "ModelBatch.hpp"
#include "Skybox.hpp"
#include "Emitter.hpp"
#include "TextNote.hpp"
#include "CloudsRenderer.hpp"
#include <assert.h> #include <assert.h>
#include <algorithm> #include <algorithm>
@ -58,8 +59,6 @@
#include <glm/gtc/matrix_transform.hpp> #include <glm/gtc/matrix_transform.hpp>
#include <memory> #include <memory>
using namespace advanced_pipeline;
inline constexpr size_t BATCH3D_CAPACITY = 4096; inline constexpr size_t BATCH3D_CAPACITY = 4096;
inline constexpr size_t MODEL_BATCH_CAPACITY = 20'000; inline constexpr size_t MODEL_BATCH_CAPACITY = 20'000;
@ -105,8 +104,7 @@ WorldRenderer::WorldRenderer(
[this](LevelEventType, Chunk* chunk) { chunksRenderer->unload(chunk); } [this](LevelEventType, Chunk* chunk) { chunksRenderer->unload(chunk); }
); );
skybox = std::make_unique<Skybox>( skybox = std::make_unique<Skybox>(
settings.graphics.skyboxResolution.get(), settings.graphics.skyboxResolution.get(), assets
assets.require<Shader>("skybox_gen")
); );
const auto& content = level.content; const auto& content = level.content;
@ -160,7 +158,7 @@ void WorldRenderer::setupWorldShader(
shader.uniform1i("u_debugNormals", false); shader.uniform1i("u_debugNormals", false);
shader.uniform1f("u_dayTime", level.getWorld()->getInfo().daytime); shader.uniform1f("u_dayTime", level.getWorld()->getInfo().daytime);
shader.uniform2f("u_lightDir", skybox->getLightDir()); shader.uniform2f("u_lightDir", skybox->getLightDir());
shader.uniform1i("u_skybox", TARGET_SKYBOX); shader.uniform1i("u_skybox", advanced_pipeline::TARGET_SKYBOX);
auto indices = level.content.getIndices(); auto indices = level.content.getIndices();
// Light emission when an emissive item is chosen // Light emission when an emissive item is chosen
@ -179,6 +177,11 @@ void WorldRenderer::setupWorldShader(
} }
} }
float WorldRenderer::calcFogFactor() const {
const auto& settings = engine.getSettings();
return 15.0f / static_cast<float>(settings.chunks.loadDistance.get() - 2);
}
void WorldRenderer::renderOpaque( void WorldRenderer::renderOpaque(
const DrawContext& ctx, const DrawContext& ctx,
const Camera& camera, const Camera& camera,
@ -187,14 +190,12 @@ void WorldRenderer::renderOpaque(
) { ) {
texts->render(ctx, camera, settings, hudVisible, false); texts->render(ctx, camera, settings, hudVisible, false);
bool culling = engine.getSettings().graphics.frustumCulling.get(); float fogFactor = calcFogFactor();
float fogFactor =
15.0f / static_cast<float>(settings.chunks.loadDistance.get() - 2);
auto& entityShader = assets.require<Shader>("entity"); auto& entityShader = assets.require<Shader>("entity");
setupWorldShader(entityShader, camera, settings, fogFactor); setupWorldShader(entityShader, camera, settings, fogFactor);
skybox->bind();
bool culling = engine.getSettings().graphics.frustumCulling.get();
if (culling) { if (culling) {
frustumCulling->update(camera.getProjView()); frustumCulling->update(camera.getProjView());
} }
@ -213,25 +214,26 @@ void WorldRenderer::renderOpaque(
auto& shader = assets.require<Shader>("main"); auto& shader = assets.require<Shader>("main");
auto& cloudsShader = assets.require<Shader>("clouds"); auto& cloudsShader = assets.require<Shader>("clouds");
auto& linesShader = assets.require<Shader>("lines");
setupWorldShader(shader, camera, settings, fogFactor); setupWorldShader(shader, camera, settings, fogFactor);
chunksRenderer->drawChunks(camera, shader); chunksRenderer->drawChunks(camera, shader);
blockWraps->draw(ctx); blockWraps->draw(ctx);
int cloudsQuality = settings.graphics.cloudsQuality.get(); if (level.environment.sky.clouds) {
if (cloudsQuality > 0) { int cloudsQuality = settings.graphics.cloudsQuality.get();
setupWorldShader(cloudsShader, camera, settings, fogFactor); if (cloudsQuality > 0) {
cloudsRenderer->draw( setupWorldShader(cloudsShader, camera, settings, fogFactor);
cloudsShader, weather, timer, fogFactor, camera, cloudsQuality cloudsRenderer->draw(
); cloudsShader, weather, timer, fogFactor, camera, cloudsQuality
);
}
} }
if (hudVisible) { if (hudVisible) {
auto& linesShader = assets.require<Shader>("lines");
renderLines(camera, linesShader, ctx); renderLines(camera, linesShader, ctx);
} }
skybox->unbind();
} }
void WorldRenderer::renderBlockSelection() { void WorldRenderer::renderBlockSelection() {
@ -291,17 +293,15 @@ void WorldRenderer::refreshSettings(Shader** shaders) {
CompileTimeShaderSettings currentSettings { CompileTimeShaderSettings currentSettings {
gbufferPipeline, gbufferPipeline,
shadowsQuality != 0, shadowsQuality != 0,
graphics.ssao.get() && gbufferPipeline graphics.ssao.get() && gbufferPipeline};
}; if (prevCTShaderSettings.advancedRender != currentSettings.advancedRender ||
if (
prevCTShaderSettings.advancedRender != currentSettings.advancedRender ||
prevCTShaderSettings.shadows != currentSettings.shadows || prevCTShaderSettings.shadows != currentSettings.shadows ||
prevCTShaderSettings.ssao != currentSettings.ssao prevCTShaderSettings.ssao != currentSettings.ssao) {
) {
std::vector<std::string> defines; std::vector<std::string> defines;
if (currentSettings.shadows) defines.emplace_back("ENABLE_SHADOWS"); if (currentSettings.shadows) defines.emplace_back("ENABLE_SHADOWS");
if (currentSettings.ssao) defines.emplace_back("ENABLE_SSAO"); if (currentSettings.ssao) defines.emplace_back("ENABLE_SSAO");
if (currentSettings.advancedRender) defines.emplace_back("ADVANCED_RENDER"); if (currentSettings.advancedRender)
defines.emplace_back("ADVANCED_RENDER");
for (size_t i = 0; shaders[i]; i++) { for (size_t i = 0; shaders[i]; i++) {
shaders[i]->recompile(defines); shaders[i]->recompile(defines);
@ -333,7 +333,8 @@ void WorldRenderer::renderFrame(
auto& entityShader = assets.require<Shader>("entity"); auto& entityShader = assets.require<Shader>("entity");
auto& cloudsShader = assets.require<Shader>("clouds"); auto& cloudsShader = assets.require<Shader>("clouds");
auto& translucentShader = assets.require<Shader>("translucent"); auto& translucentShader = assets.require<Shader>("translucent");
auto& deferredShader = assets.require<PostEffect>("deferred_lighting").getShader(); auto& deferredShader =
assets.require<PostEffect>("deferred_lighting").getShader();
const auto& settings = engine.getSettings(); const auto& settings = engine.getSettings();
@ -343,8 +344,7 @@ void WorldRenderer::renderFrame(
&cloudsShader, &cloudsShader,
&translucentShader, &translucentShader,
&deferredShader, &deferredShader,
nullptr nullptr};
};
refreshSettings(affectedShaders); refreshSettings(affectedShaders);
@ -356,6 +356,7 @@ void WorldRenderer::renderFrame(
float random = rand() / static_cast<float>(RAND_MAX); float random = rand() / static_cast<float>(RAND_MAX);
skybox->refresh( skybox->refresh(
level.environment,
pctx, pctx,
worldInfo.daytime, worldInfo.daytime,
mie, mie,
@ -374,20 +375,19 @@ void WorldRenderer::renderFrame(
{ {
DrawContext wctx = pctx.sub(); DrawContext wctx = pctx.sub();
postProcessing.use(wctx, gbufferPipeline); postProcessing.use(wctx, gbufferPipeline);
display::clearDepth(); display::clearDepth();
/* Main opaque pass (GBuffer pass) */ { /* Main opaque pass (GBuffer pass) */ {
DrawContext ctx = wctx.sub(); DrawContext ctx = wctx.sub();
ctx.setDepthTest(true); ctx.setDepthTest(true);
ctx.setCullFace(true); ctx.setCullFace(true);
ctx.useTexture(advanced_pipeline::TARGET_SKYBOX, skybox->getCubemap());
ctx.useTexture(advanced_pipeline::TARGET_COLOR, nullptr);
renderOpaque(ctx, camera, settings, hudVisible); renderOpaque(ctx, camera, settings, hudVisible);
} }
texts->render(pctx, camera, settings, hudVisible, true); texts->render(pctx, camera, settings, hudVisible, true);
} }
skybox->bind(); float fogFactor = calcFogFactor();
float fogFactor =
15.0f / static_cast<float>(settings.chunks.loadDistance.get() - 2);
if (gbufferPipeline) { if (gbufferPipeline) {
deferredShader.use(); deferredShader.use();
setupWorldShader(deferredShader, camera, settings, fogFactor); setupWorldShader(deferredShader, camera, settings, fogFactor);
@ -396,6 +396,7 @@ void WorldRenderer::renderFrame(
{ {
DrawContext ctx = pctx.sub(); DrawContext ctx = pctx.sub();
ctx.setDepthTest(true); ctx.setDepthTest(true);
ctx.useTexture(advanced_pipeline::TARGET_COLOR, nullptr);
if (gbufferPipeline) { if (gbufferPipeline) {
postProcessing.bindDepthBuffer(); postProcessing.bindDepthBuffer();
@ -404,7 +405,9 @@ void WorldRenderer::renderFrame(
} }
// Background sky plane // Background sky plane
skybox->draw(ctx, camera, assets, worldInfo.daytime, clouds); skybox->draw(
level.environment, ctx, camera, worldInfo.daytime, clouds
);
auto& linesShader = assets.require<Shader>("lines"); auto& linesShader = assets.require<Shader>("lines");
linesShader.use(); linesShader.use();
@ -416,10 +419,12 @@ void WorldRenderer::renderFrame(
linesShader.uniformMatrix("u_projview", projView); linesShader.uniformMatrix("u_projview", projView);
lineBatch->flush(); lineBatch->flush();
skybox->bind(); DrawContext wctx = ctx.sub();
wctx.useTexture(advanced_pipeline::TARGET_SKYBOX, skybox->getCubemap());
wctx.useTexture(advanced_pipeline::TARGET_COLOR, nullptr);
// Translucent blocks // Translucent blocks
{ {
auto sctx = ctx.sub(); auto sctx = wctx.sub();
sctx.setCullFace(true); sctx.setCullFace(true);
translucentShader.use(); translucentShader.use();
setupWorldShader(translucentShader, camera, settings, fogFactor); setupWorldShader(translucentShader, camera, settings, fogFactor);
@ -430,20 +435,21 @@ void WorldRenderer::renderFrame(
entityShader.use(); entityShader.use();
setupWorldShader(entityShader, camera, settings, fogFactor); setupWorldShader(entityShader, camera, settings, fogFactor);
std::array<const WeatherPreset*, 2> weatherInstances {&weather.a, &weather.b}; std::array<const WeatherPreset*, 2> weatherInstances {
&weather.a, &weather.b};
for (const auto& weather : weatherInstances) { for (const auto& weather : weatherInstances) {
float maxIntensity = weather->fall.maxIntensity; float maxIntensity = weather->fall.maxIntensity;
float zero = weather->fall.minOpacity; float zero = weather->fall.minOpacity;
float one = weather->fall.maxOpacity; float one = weather->fall.maxOpacity;
float t = (weather->intensity * (one - zero)) * maxIntensity + zero; float t = (weather->intensity * (one - zero)) * maxIntensity + zero;
entityShader.uniform1i("u_alphaClip", weather->fall.opaque); entityShader.uniform1i("u_alphaClip", weather->fall.opaque);
entityShader.uniform1f("u_opacity", weather->fall.opaque ? t * t : t); entityShader.uniform1f(
"u_opacity", weather->fall.opaque ? t * t : t
);
if (weather->intensity > 1.e-3f && !weather->fall.texture.empty()) { if (weather->intensity > 1.e-3f && !weather->fall.texture.empty()) {
precipitation->render(camera, *weather); precipitation->render(camera, *weather);
} }
} }
skybox->unbind();
glBindFramebuffer(GL_FRAMEBUFFER, 0); glBindFramebuffer(GL_FRAMEBUFFER, 0);
} }
postProcessing.render(pctx, assets, timer, camera); postProcessing.render(pctx, assets, timer, camera);
@ -452,6 +458,9 @@ void WorldRenderer::renderFrame(
DrawContext ctx = pctx.sub(); DrawContext ctx = pctx.sub();
ctx.setDepthTest(true); ctx.setDepthTest(true);
ctx.setCullFace(true); ctx.setCullFace(true);
ctx.useTexture(advanced_pipeline::TARGET_SKYBOX, skybox->getCubemap());
ctx.useTexture(advanced_pipeline::TARGET_COLOR, nullptr);
display::clearDepth();
// prepare modified HUD camera // prepare modified HUD camera
Camera hudcam = camera; Camera hudcam = camera;
@ -459,15 +468,8 @@ void WorldRenderer::renderFrame(
hudcam.setFov(0.9f); hudcam.setFov(0.9f);
hudcam.position = {}; hudcam.position = {};
hands->render(camera);
display::clearDepth();
setupWorldShader(entityShader, hudcam, engine.getSettings(), 0.0f); setupWorldShader(entityShader, hudcam, engine.getSettings(), 0.0f);
hands->render(camera);
skybox->bind();
modelBatch->render();
modelBatch->setLightsOffset(glm::vec3());
skybox->unbind();
} }
renderBlockOverlay(pctx); renderBlockOverlay(pctx);
@ -487,9 +489,8 @@ void WorldRenderer::renderBlockOverlay(const DrawContext& wctx) {
if (def.overlayTexture.empty()) { if (def.overlayTexture.empty()) {
return; return;
} }
auto textureRegion = util::get_texture_region( auto textureRegion =
assets, def.overlayTexture, "blocks:notfound" util::get_texture_region(assets, def.overlayTexture, "blocks:notfound");
);
DrawContext ctx = wctx.sub(); DrawContext ctx = wctx.sub();
ctx.setDepthTest(false); ctx.setDepthTest(false);
ctx.setCullFace(false); ctx.setCullFace(false);

View file

@ -99,6 +99,8 @@ class WorldRenderer {
); );
void refreshSettings(Shader** shaders); void refreshSettings(Shader** shaders);
float calcFogFactor() const;
public: public:
std::unique_ptr<ParticlesRenderer> particles; std::unique_ptr<ParticlesRenderer> particles;
std::unique_ptr<TextsRenderer> texts; std::unique_ptr<TextsRenderer> texts;

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

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

@ -33,6 +33,8 @@ namespace io {
} }
} }
path(std::string_view str) : path(std::string(str)) {}
path(const char* str) : path(std::string(str)) {} path(const char* str) : path(std::string(str)) {}
bool operator==(const std::string& other) const { bool operator==(const std::string& other) const {

View file

@ -25,7 +25,7 @@ const uint MIN_SURROUNDING = 9;
ChunksController::ChunksController(Level& level) ChunksController::ChunksController(Level& level)
: level(level), : level(level),
generator(std::make_unique<WorldGenerator>( generator(std::make_unique<WorldGenerator>(
level.content.generators.require(level.getWorld()->getGenerator()), level.content.generators.require(level.environment.generator),
level.content, level.content,
level.getWorld()->getSeed() level.getWorld()->getSeed()
)) {} )) {}

View file

@ -179,7 +179,7 @@ void EngineController::onMissingContent(
if (engine.isHeadless()) { if (engine.isHeadless()) {
throw std::runtime_error( throw std::runtime_error(
"missing content: " + "missing content: " +
json::stringify(create_missing_content_report(report), true, " ") util::quote(json::stringify(create_missing_content_report(report), true, " "))
); );
} else { } else {
engine.setScreen(std::make_shared<MenuScreen>(engine)); engine.setScreen(std::make_shared<MenuScreen>(engine));
@ -268,7 +268,7 @@ inline uint64_t str2seed(const std::string& seedstr) {
void EngineController::createWorld( void EngineController::createWorld(
const std::string& name, const std::string& name,
const std::string& seedstr, const std::string& seedstr,
const std::string& generatorID const std::string& environment
) { ) {
uint64_t seed = str2seed(seedstr); uint64_t seed = str2seed(seedstr);
@ -279,17 +279,31 @@ void EngineController::createWorld(
paths.setCurrentWorldFolder(folder); paths.setCurrentWorldFolder(folder);
engine.getContentControl().loadContent(); engine.getContentControl().loadContent();
}); });
const auto& generators = engine.getContentControl().get()->generators;
// generator-based environments must die in 1.0
bool genBasedEnv = false;
if (auto genDef = generators.find(environment)) {
genBasedEnv = true;
}
auto& contentControl = engine.getContentControl(); auto& contentControl = engine.getContentControl();
auto level = World::create( auto level = World::create(
name, name,
generatorID, genBasedEnv ? "" : environment,
genBasedEnv ? environment : "",
folder, folder,
seed, seed,
engine.getSettings(), engine.getSettings(),
*contentControl.get(), *contentControl.get(),
contentControl.getContentPacks() contentControl.getContentPacks()
); );
// generator-based environment initialization
if (genBasedEnv) {
level->environment.generator = environment;
}
if (!engine.isHeadless()) { if (!engine.isHeadless()) {
level->players->create(localPlayer); level->players->create(localPlayer);
} }

View file

@ -149,7 +149,7 @@ void LevelController::saveWorld() {
world->wfile->createDirectories(); world->wfile->createDirectories();
scripting::on_world_save(); scripting::on_world_save();
level->onSave(); level->onSave();
level->getWorld()->write(level.get()); level->getWorld()->write(*level);
} }
void LevelController::onWorldQuit() { void LevelController::onWorldQuit() {

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[];

View file

@ -20,8 +20,14 @@
#include "window/Window.hpp" #include "window/Window.hpp"
#include "world/Level.hpp" #include "world/Level.hpp"
#include <array>
using namespace scripting; using namespace scripting;
namespace {
static std::array<std::unique_ptr<Process>, MAX_SUBPROCESSES> processes;
}
/// @brief Check if content is loaded /// @brief Check if content is loaded
static int l_is_content_loaded(lua::State* L) { static int l_is_content_loaded(lua::State* L) {
return lua::pushboolean(L, content != nullptr); return lua::pushboolean(L, content != nullptr);
@ -337,6 +343,10 @@ static int l_start_debug_instance(lua::State* L) {
if (!engine->getProject().permissions.has(Permissions::DEBUGGING)) { if (!engine->getProject().permissions.has(Permissions::DEBUGGING)) {
throw std::runtime_error("project has no debugging permission"); throw std::runtime_error("project has no debugging permission");
} }
const auto& params = engine->getCoreParameters();
if (params.subProcessDepth >= MAX_SUBPROCESS_DEPTH) {
throw std::runtime_error("max subprocess depth exceeded");
}
int port = lua::tointeger(L, 1); int port = lua::tointeger(L, 1);
if (port == 0) { if (port == 0) {
@ -357,6 +367,8 @@ static int l_start_debug_instance(lua::State* L) {
"--res", paths.getResourcesFolder().u8string(), "--res", paths.getResourcesFolder().u8string(),
"--dir", paths.getUserFilesFolder().u8string(), "--dir", paths.getUserFilesFolder().u8string(),
"--dbg-server", "tcp:" + std::to_string(port), "--dbg-server", "tcp:" + std::to_string(port),
"--sub-depth", std::to_string(engine->getCoreParameters()
.subProcessDepth + 1),
}; };
if (!projectPath.empty()) { if (!projectPath.empty()) {
args.emplace_back("--project"); args.emplace_back("--project");
@ -365,11 +377,76 @@ static int l_start_debug_instance(lua::State* L) {
platform::new_engine_instance( platform::new_engine_instance(
std::move(args), std::move(args),
outputPath.empty() ? "" : io::resolve(std::string(outputPath)) outputPath.empty() ? "" : io::resolve(std::string(outputPath)),
false
); );
return lua::pushinteger(L, port); return lua::pushinteger(L, port);
} }
static int l_start_background_instance(lua::State* L) {
if (!engine->getProject().permissions.has(Permissions::SUB_INSTANCES)) {
throw std::runtime_error("project has no sub-instances permission");
}
const auto& params = engine->getCoreParameters();
if (params.subProcessDepth >= MAX_SUBPROCESS_DEPTH) {
throw std::runtime_error("max subprocess depth exceeded");
}
auto scriptPath = lua::require_lstring(L, 1);
io::path outputPath = lua::isstring(L, 2) ? lua::require_lstring(L, 2) : "";
const auto& paths = engine->getPaths();
std::vector<std::string> args {
"--headless",
"--res", paths.getResourcesFolder().u8string(),
"--dir", paths.getUserFilesFolder().u8string(),
"--script", io::resolve(scriptPath).u8string(),
"--sub-depth", std::to_string(engine->getCoreParameters()
.subProcessDepth + 1),
"--log", io::resolve(outputPath).u8string(),
};
args.emplace_back("--project");
args.emplace_back(io::resolve(engine->getProject().path).u8string());
int handle = -1;
for (int i = 0; i < ::processes.size(); i++) {
if (!::processes[i] || !::processes[i]->isActive()) {
handle = i;
break;
}
}
if (handle == -1) {
throw std::runtime_error("sub-processes limit exceeded");
}
::processes[handle] =
platform::new_engine_instance(std::move(args), "", true);
return lua::pushinteger(L, handle);
}
static int l_is_instance_alive(lua::State* L) {
int handle = lua::tointeger(L, 1);
if (handle < 0 || handle >= ::processes.size()) {
throw std::runtime_error("invalid process handle");
}
auto& process = ::processes[handle];
return lua::pushboolean(L, process && process->isActive());
}
static int l_terminate_instance(lua::State* L) {
int handle = lua::tointeger(L, 1);
if (handle < 0 || handle >= ::processes.size()) {
throw std::runtime_error("invalid process handle");
}
auto& process = ::processes[handle];
if (process == nullptr) {
return lua::pushboolean(L, false);
}
bool active = process->isActive();
process.reset();
return lua::pushboolean(L, active);
}
const luaL_Reg applib[] = { const luaL_Reg applib[] = {
/// content /// content
{"is_content_loaded", lua::wrap<l_is_content_loaded>}, {"is_content_loaded", lua::wrap<l_is_content_loaded>},
@ -402,5 +479,8 @@ const luaL_Reg applib[] = {
{"get_version", lua::wrap<l_get_version>}, {"get_version", lua::wrap<l_get_version>},
{"create_memory_device", lua::wrap<l_create_memory_device>}, {"create_memory_device", lua::wrap<l_create_memory_device>},
{"start_debug_instance", lua::wrap<l_start_debug_instance>}, {"start_debug_instance", lua::wrap<l_start_debug_instance>},
{"start_background_instance", lua::wrap<l_start_background_instance>},
{"is_instance_alive", lua::wrap<l_is_instance_alive>},
{"terminate_instance", lua::wrap<l_terminate_instance>},
{nullptr, nullptr} {nullptr, nullptr}
}; };

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