Merge branch 'main' into animation

This commit is contained in:
MihailRis 2026-07-25 22:00:00 +03:00
commit 8a9b4a9e9c
55 changed files with 1660 additions and 590 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

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

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

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

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

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

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

@ -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
@ -334,6 +338,10 @@ else
os.pid = ffi.C.getpid() os.pid = ffi.C.getpid()
end end
require("core:io_stream").wrap_bytearray = require "core:internal/stream_providers/bytearray"
network.__as_stream = require "core:internal/stream_providers/socket"
math.randomseed(time.uptime() * 1536227939) math.randomseed(time.uptime() * 1536227939)
rules = require "core:internal/rules" rules = require "core:internal/rules"

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -26,6 +26,11 @@ Font::Font(
pages(std::move(pages)), pages(std::move(pages)),
glyphs(std::move(glyphs)), glyphs(std::move(glyphs)),
fontFile(std::move(fontFile)) { fontFile(std::move(fontFile)) {
if (this->fontFile.has_value()) {
if (auto fontPtr = this->fontFile->lock()) {
monospace = fontPtr->isMonospace();
}
}
} }
Font::~Font() = default; Font::~Font() = default;
@ -34,6 +39,10 @@ int Font::getYOffset() const {
return yoffset; return yoffset;
} }
bool Font::isMonospace() const {
return monospace;
}
int Font::getLineHeight() const { int Font::getLineHeight() const {
return lineHeight; return lineHeight;
} }
@ -53,7 +62,7 @@ bool Font::isPrintableChar(uint codepoint) const {
int FontMetrics::calcWidth(std::wstring_view text, size_t offset, size_t length) const { int FontMetrics::calcWidth(std::wstring_view text, size_t offset, size_t length) const {
auto font = this->font.has_value() ? this->font->lock() : nullptr; auto font = this->font.has_value() ? this->font->lock() : nullptr;
if (font == nullptr) { if (font == nullptr || font->isMonospace()) {
return std::min(text.length() - offset, length) * _glyphInterval; return std::min(text.length() - offset, length) * _glyphInterval;
} }
int totalWidth = 0; int totalWidth = 0;

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

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

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

@ -10,7 +10,7 @@
#include "util/stringutil.hpp" #include "util/stringutil.hpp"
#include "api_lua.hpp" #include "api_lua.hpp"
#include "../lua_engine.hpp" #include "../lua_engine.hpp"
#include "logic/scripting/descriptors_manager.hpp" #include "logic/scripting/io_descriptors.hpp"
namespace fs = std::filesystem; namespace fs = std::filesystem;
using namespace scripting; using namespace scripting;
@ -263,13 +263,13 @@ static int l_open_descriptor(lua::State* L) {
std::vector<char> buffer; std::vector<char> buffer;
if(wplusMode) { if(wplusMode) {
int temp_descriptor = scripting::descriptors_manager::open_descriptor(path, false, true); int temp_descriptor = io_descriptors::open_descriptor(path, false, true);
if (temp_descriptor == -1) { if (temp_descriptor == -1) {
throw std::runtime_error("failed to open descriptor for initial reading"); throw std::runtime_error("failed to open descriptor for initial reading");
} }
auto* in_stream = scripting::descriptors_manager::get_input(temp_descriptor); auto* in_stream = io_descriptors::get_input(temp_descriptor);
in_stream->seekg(0, std::ios::end); in_stream->seekg(0, std::ios::end);
std::streamsize size = in_stream->tellg(); std::streamsize size = in_stream->tellg();
@ -278,17 +278,17 @@ static int l_open_descriptor(lua::State* L) {
buffer.resize(size); buffer.resize(size);
in_stream->read(buffer.data(), size); in_stream->read(buffer.data(), size);
scripting::descriptors_manager::close(temp_descriptor); io_descriptors::close(temp_descriptor);
} }
int descriptor = scripting::descriptors_manager::open_descriptor(path, write, read); int descriptor = io_descriptors::open_descriptor(path, write, read);
if(descriptor == -1) { if(descriptor == -1) {
throw std::runtime_error("failed to open descriptor"); throw std::runtime_error("failed to open descriptor");
} }
if(wplusMode) { if(wplusMode) {
auto* out_stream = scripting::descriptors_manager::get_output(descriptor); auto* out_stream = io_descriptors::get_output(descriptor);
out_stream->write(buffer.data(), buffer.size()); out_stream->write(buffer.data(), buffer.size());
out_stream->flush(); out_stream->flush();
} }
@ -297,51 +297,31 @@ static int l_open_descriptor(lua::State* L) {
} }
static int l_has_descriptor(lua::State* L) { static int l_has_descriptor(lua::State* L) {
return lua::pushboolean(L, scripting::descriptors_manager::has_descriptor(lua::tointeger(L, 1))); return lua::pushboolean(L, io_descriptors::has_descriptor(lua::tointeger(L, 1)));
} }
static int l_read_descriptor(lua::State* L) { static int l_read_descriptor(lua::State* L) {
int descriptor = lua::tointeger(L, 1); int descriptor = lua::tointeger(L, 1);
if (!scripting::descriptors_manager::has_descriptor(descriptor)) {
throw std::runtime_error("unknown descriptor");
}
if (!scripting::descriptors_manager::is_readable(descriptor)) {
throw std::runtime_error("descriptor is not readable");
}
int maxlen = lua::tointeger(L, 2); int maxlen = lua::tointeger(L, 2);
auto* stream = scripting::descriptors_manager::get_input(descriptor); auto& stream = io_descriptors::require_input(descriptor);
if (stream.eof()) {
stream.clear();
}
util::Buffer<char> buffer(maxlen); util::Buffer<char> buffer(maxlen);
stream.read(buffer.data(), maxlen);
stream->read(buffer.data(), maxlen); std::streamsize read_len = stream.gcount();
std::streamsize read_len = stream->gcount();
return lua::create_bytearray(L, buffer.data(), read_len); return lua::create_bytearray(L, buffer.data(), read_len);
} }
static int l_write_descriptor(lua::State* L) { static int l_write_descriptor(lua::State* L) {
int descriptor = lua::tointeger(L, 1); int descriptor = lua::tointeger(L, 1);
if (!scripting::descriptors_manager::has_descriptor(descriptor)) {
throw std::runtime_error("unknown descriptor");
}
if (!scripting::descriptors_manager::is_writeable(descriptor)) {
throw std::runtime_error("descriptor is not writeable");
}
auto data = lua::bytearray_as_string(L, 2); auto data = lua::bytearray_as_string(L, 2);
auto* stream = scripting::descriptors_manager::get_output(descriptor); auto& stream = io_descriptors::require_output(descriptor);
stream.write(data.data(), static_cast<std::streamsize>(data.size()));
stream->write(data.data(), static_cast<std::streamsize>(data.size())); if (!stream.good()) {
if (!stream->good()) {
throw std::runtime_error("failed to write to stream"); throw std::runtime_error("failed to write to stream");
} }
return 0; return 0;
@ -350,12 +330,13 @@ static int l_write_descriptor(lua::State* L) {
static int l_seek_descriptor(lua::State* L) { static int l_seek_descriptor(lua::State* L) {
int descriptor = lua::tointeger(L, 1); int descriptor = lua::tointeger(L, 1);
if (!scripting::descriptors_manager::has_descriptor(descriptor)) { if (!io_descriptors::has_descriptor(descriptor)) {
throw std::runtime_error("unknown descriptor"); throw std::runtime_error("unknown descriptor");
} }
std::string mode = lua::require_string(L, 2); auto mode = lua::require_string(L, 2);
std::ios_base::seekdir dir; std::ios_base::seekdir dir;
auto position = lua::tointeger(L, 3);
switch (mode[0]) { switch (mode[0]) {
case 'b': case 'b':
@ -371,45 +352,81 @@ static int l_seek_descriptor(lua::State* L) {
throw std::runtime_error("invalid seek mode"); throw std::runtime_error("invalid seek mode");
} }
auto* stream = scripting::descriptors_manager::get_output(descriptor); if (io_descriptors::is_writeable(descriptor)) {
auto& stream = io_descriptors::require_output(descriptor);
stream.seekp(position, dir);
if (!stream.good()) {
throw std::runtime_error("failed to seek stream");
}
}
if (io_descriptors::is_readable(descriptor)) {
auto& stream = io_descriptors::require_input(descriptor);
stream.seekg(position, dir);
if (!stream.good()) {
throw std::runtime_error("failed to seek stream");
}
}
return 0;
}
stream->seekp(lua::tointeger(L, 3), dir); static int l_tell_descriptor(lua::State* L) {
int descriptor = lua::tointeger(L, 1);
if (!stream->good()) { if (!io_descriptors::has_descriptor(descriptor)) {
throw std::runtime_error("failed to seek stream"); throw std::runtime_error("unknown descriptor");
} }
return 0; if (io_descriptors::is_writeable(descriptor)) {
auto& stream = io_descriptors::require_output(descriptor);
return lua::pushinteger(L, stream.tellp());
} else {
auto& stream = io_descriptors::require_input(descriptor);
return lua::pushinteger(L, stream.tellg());
}
} }
static int l_flush_descriptor(lua::State* L) { static int l_flush_descriptor(lua::State* L) {
int descriptor = lua::tointeger(L, 1); int descriptor = lua::tointeger(L, 1);
if (!scripting::descriptors_manager::has_descriptor(descriptor)) { if (!io_descriptors::has_descriptor(descriptor)) {
throw std::runtime_error("unknown descriptor"); throw std::runtime_error("unknown descriptor");
} }
if (!scripting::descriptors_manager::is_writeable(descriptor)) { if (!io_descriptors::is_writeable(descriptor)) {
throw std::runtime_error("descriptor is not writeable"); throw std::runtime_error("descriptor is not writeable");
} }
scripting::descriptors_manager::flush(descriptor); io_descriptors::flush(descriptor);
return 0; return 0;
} }
static int l_available_descriptor(lua::State* L) {
int descriptor = lua::tointeger(L, 1);
if (!io_descriptors::has_descriptor(descriptor)) {
throw std::runtime_error("unknown descriptor");
}
if (!io_descriptors::is_readable(descriptor)) {
throw std::runtime_error("descriptor is not readable");
}
return lua::pushinteger(L, io_descriptors::available(descriptor));
}
static int l_close_descriptor(lua::State* L) { static int l_close_descriptor(lua::State* L) {
int descriptor = lua::tointeger(L, 1); int descriptor = lua::tointeger(L, 1);
if (!scripting::descriptors_manager::has_descriptor(descriptor)) { if (!io_descriptors::has_descriptor(descriptor)) {
throw std::runtime_error("unknown descriptor"); throw std::runtime_error("unknown descriptor");
} }
scripting::descriptors_manager::close(descriptor); io_descriptors::close(descriptor);
return 0; return 0;
} }
static int l_close_all_descriptors(lua::State* L) { static int l_close_all_descriptors(lua::State* L) {
scripting::descriptors_manager::close_all_descriptors(); io_descriptors::close_all_descriptors();
return 0; return 0;
} }
@ -442,7 +459,9 @@ const luaL_Reg filelib[] = {
{"__read_descriptor", lua::wrap<l_read_descriptor>}, {"__read_descriptor", lua::wrap<l_read_descriptor>},
{"__write_descriptor", lua::wrap<l_write_descriptor>}, {"__write_descriptor", lua::wrap<l_write_descriptor>},
{"__seek_descriptor", lua::wrap<l_seek_descriptor>}, {"__seek_descriptor", lua::wrap<l_seek_descriptor>},
{"__tell_descriptor", lua::wrap<l_tell_descriptor>},
{"__flush_descriptor", lua::wrap<l_flush_descriptor>}, {"__flush_descriptor", lua::wrap<l_flush_descriptor>},
{"__available_descriptor", lua::wrap<l_available_descriptor>},
{"__close_descriptor", lua::wrap<l_close_descriptor>}, {"__close_descriptor", lua::wrap<l_close_descriptor>},
{"__close_all_descriptors", lua::wrap<l_close_all_descriptors>}, {"__close_all_descriptors", lua::wrap<l_close_all_descriptors>},
{nullptr, nullptr} {nullptr, nullptr}

View file

@ -420,15 +420,19 @@ static int p_get_data(UINode* node, lua::State* L) {
return 0; return 0;
} }
static const std::string& request_node_id(const DocumentNode& docnode) { static const std::string& request_node_id(UiDocument& document, UINode& node) {
std::string id = docnode.node->getId(); const std::string& id = node.getId();
if (id.empty()) { if (id.empty()) {
id = "#" + std::to_string( node.setId( "#" + std::to_string(
reinterpret_cast<std::ptrdiff_t>(docnode.node.get())); reinterpret_cast<std::ptrdiff_t>(&node)));
document.pushIndices(node.shared_from_this());
return node.getId();
} }
docnode.node->setId(std::move(id)); return id;
docnode.document->pushIndices(docnode.node); }
return docnode.node->getId();
static const std::string& request_node_id(const DocumentNode& docnode) {
return request_node_id(*docnode.document, *docnode.node);
} }
/// @brief Push UI-document node object to stack /// @brief Push UI-document node object to stack
@ -443,15 +447,14 @@ static int push_document_node(lua::State* L, const std::string& id) {
static int p_get_parent(UINode* node, lua::State* L) { static int p_get_parent(UINode* node, lua::State* L) {
auto parent = node->getParent(); auto parent = node->getParent();
if (!parent) { if (parent == nullptr) {
return 0; return 0;
} }
auto docname = lua::require_string(L, 1); auto docname = lua::require_string(L, 1);
auto element = lua::require_string(L, 2); auto element = lua::require_string(L, 2);
auto docnode = get_document_node_impl(L, docname, element); auto docnode = get_document_node_impl(L, docname, element);
const auto& id = request_node_id(docnode); const auto& id = request_node_id(*docnode.document, *parent);
return push_document_node(L, id); return push_document_node(L, id);
} }
@ -1144,7 +1147,7 @@ static int l_gui_load_document(lua::State* L) {
auto document = documentPtr.get(); auto document = documentPtr.get();
engine->requireAssets().store(std::move(documentPtr), alias); engine->requireAssets().store(std::move(documentPtr), alias);
scripting::on_ui_open(document, {args}); scripting::on_ui_open(*document, {args});
return 0; return 0;
} }

View file

@ -0,0 +1,45 @@
#include "api_lua.hpp"
#include "engine/Engine.hpp"
#include "window/input.hpp"
#include <iostream>
using namespace scripting;
static int l_set_button_pressed(lua::State* L) {
int button = lua::touinteger(L, 1);
int x = lua::tointeger(L, 2);
int y = lua::tointeger(L, 3);
bool pressed = lua::toboolean(L, 4);
auto& input = engine->getInput();
input.simulateCursorPos(x, y);
input.simulateClick(button, pressed);
return 0;
}
static int l_set_key_pressed(lua::State* L) {
auto key = lua::require_string(L, 1);
bool pressed = lua::toboolean(L, 2);
auto& input = engine->getInput();
input.simulateKey(input_util::keycode_from(key), pressed);
return 0;
}
static int l_enter_text(lua::State* L) {
auto string = lua::require_wstring(L, 1);
auto& input = engine->getInput();
for (auto chr : string) {
input.simulateCodepoint(chr);
}
return 0;
}
const luaL_Reg testlib[] = {
{"set_button_pressed", lua::wrap<l_set_button_pressed>},
{"set_key_pressed", lua::wrap<l_set_key_pressed>},
{"enter_text", lua::wrap<l_enter_text>},
{nullptr, nullptr}
};

View file

@ -129,6 +129,9 @@ static int l_get_generator(lua::State* L) {
} }
static int l_get_chunk_data(lua::State* L) { static int l_get_chunk_data(lua::State* L) {
if (level == nullptr) {
throw std::runtime_error("world is not open");
}
int x = static_cast<int>(lua::tointeger(L, 1)); int x = static_cast<int>(lua::tointeger(L, 1));
int z = static_cast<int>(lua::tointeger(L, 2)); int z = static_cast<int>(lua::tointeger(L, 2));
const auto& chunk = level->chunks->getChunk(x, z); const auto& chunk = level->chunks->getChunk(x, z);
@ -175,7 +178,7 @@ static void integrate_chunk_client(Chunk& chunk) {
static int l_set_chunk_data(lua::State* L) { static int l_set_chunk_data(lua::State* L) {
if (level == nullptr) { if (level == nullptr) {
throw std::runtime_error("no open world"); throw std::runtime_error("world is not open");
} }
int x = static_cast<int>(lua::tointeger(L, 1)); int x = static_cast<int>(lua::tointeger(L, 1));
@ -201,7 +204,7 @@ static int l_set_chunk_data(lua::State* L) {
static int l_save_chunk_data(lua::State* L) { static int l_save_chunk_data(lua::State* L) {
if (level == nullptr) { if (level == nullptr) {
throw std::runtime_error("no open world"); throw std::runtime_error("world is not open");
} }
int x = static_cast<int>(lua::tointeger(L, 1)); int x = static_cast<int>(lua::tointeger(L, 1));

View file

@ -19,6 +19,7 @@ namespace {
debug::Logger logger("lua-state"); debug::Logger logger("lua-state");
lua::State* main_thread = nullptr; lua::State* main_thread = nullptr;
bool headless_mode = false; bool headless_mode = false;
bool test_mode = false;
const std::unordered_map<std::string, std::string>* project_args; const std::unordered_map<std::string, std::string>* project_args;
} }
@ -70,12 +71,12 @@ static void create_libs(State* L, StateType stateType) {
openlib(L, "yaml", yamllib); openlib(L, "yaml", yamllib);
openlib(L, "__vc_app", applib); openlib(L, "__vc_app", applib);
lua::getglobal(L, "__vc_app"); getglobal(L, "__vc_app");
lua::setregistry(L, "app"); setregistry(L, "app");
if (stateType == StateType::SCRIPT) { if (stateType == StateType::SCRIPT) {
lua::getregistry(L, "app"); getregistry(L, "app");
lua::setglobal(L, "app"); setglobal(L, "app");
} }
if (stateType == StateType::BASE || stateType == StateType::SCRIPT) { if (stateType == StateType::BASE || stateType == StateType::SCRIPT) {
openlib(L, "assets", assetslib); openlib(L, "assets", assetslib);
@ -100,6 +101,10 @@ static void create_libs(State* L, StateType stateType) {
openlib(L, "__transform", transformlib); openlib(L, "__transform", transformlib);
} }
if (::test_mode) {
openlib(L, "test", testlib);
}
addfunc(L, "print", lua::wrap<l_print>); addfunc(L, "print", lua::wrap<l_print>);
addfunc(L, "crc32", lua::wrap<l_crc32>); addfunc(L, "crc32", lua::wrap<l_crc32>);
} }
@ -192,6 +197,7 @@ void lua::initialize(const EnginePaths& paths, const CoreParameters& params) {
logger.info() << LUAJIT_VERSION; logger.info() << LUAJIT_VERSION;
headless_mode = params.headless; headless_mode = params.headless;
test_mode = params.testMode;
project_args = &params.projectArgs; project_args = &params.projectArgs;
main_thread = create_state( main_thread = create_state(
paths, params.headless ? StateType::SCRIPT : StateType::BASE paths, params.headless ? StateType::SCRIPT : StateType::BASE

View file

@ -591,36 +591,46 @@ bool scripting::on_item_break_block(
); );
} }
void scripting::on_ui_open( static void call_layout_event(const UiDocument& layout, const std::vector<dv::value>& args, const std::string& eventName) {
UiDocument* layout, std::vector<dv::value> args auto L = lua::get_main_state();
) { lua::pushenv(L, *layout.getEnvironment());
auto argsptr = if (lua::getfield(L, eventName)) {
std::make_shared<std::vector<dv::value>>(std::move(args)); for (const auto& arg : args) {
std::string name = layout->getId() + ".open"; lua::pushvalue(L, arg);
lua::emit_event(lua::get_main_state(), name, [=](auto L) {
for (const auto& value : *argsptr) {
lua::pushvalue(L, value);
} }
return argsptr->size(); lua::call_nothrow(L, args.size(), 0);
}); }
lua::pop(L);
}
void scripting::on_ui_open(
const UiDocument& layout, std::vector<dv::value> args
) {
if (layout.getScript().onopen) {
call_layout_event(layout, args, "on_open");
}
} }
void scripting::on_ui_progress( void scripting::on_ui_progress(
UiDocument* layout, int workDone, int workTotal const UiDocument& layout, int workDone, int workTotal
) { ) {
std::string name = layout->getId() + ".progress"; if (layout.getScript().onprogress){
lua::emit_event(lua::get_main_state(), name, [=](auto L) { call_layout_event(layout, {workDone, workTotal}, "on_progress");
lua::pushinteger(L, workDone); }
lua::pushinteger(L, workTotal);
return 2;
});
} }
void scripting::on_ui_close(UiDocument* layout, Inventory* inventory) { void scripting::on_ui_close(const UiDocument& layout, Inventory* inventory) {
std::string name = layout->getId() + ".close"; if (layout.getScript().onclose) {
lua::emit_event(lua::get_main_state(), name, [inventory](auto L) { call_layout_event(
return lua::pushinteger(L, inventory ? inventory->getId() : 0); layout, {inventory ? inventory->getId() : 0}, "on_close"
}); );
}
}
void scripting::on_ui_destroy(const UiDocument& layout) {
if (layout.getScript().ondestroy) {
call_layout_event(layout, {}, "on_destroy");
}
} }
void scripting::on_scripts_loading() { void scripting::on_scripts_loading() {
@ -805,11 +815,14 @@ void scripting::load_layout_script(
) { ) {
int env = *senv; int env = *senv;
lua::pop(lua::get_main_state(), load_script(env, "layout", file, fileName)); auto L = lua::get_main_state();
script.onopen = register_event(env, "on_open", prefix + ".open"); lua::pop(L, load_script(env, "layout", file, fileName));
script.onprogress = lua::pushenv(L, env);
register_event(env, "on_progress", prefix + ".progress"); script.onopen = lua::hasfield(L, "on_open");
script.onclose = register_event(env, "on_close", prefix + ".close"); script.onprogress = lua::hasfield(L, "on_progress");
script.onclose = lua::hasfield(L, "on_close");
script.ondestroy = lua::hasfield(L, "on_destroy");
lua::pop(L);
} }
void scripting::close() { void scripting::close() {

View file

@ -153,13 +153,16 @@ namespace scripting {
void on_attacked(const Entity& entity, Player* player, entityid_t attacker); void on_attacked(const Entity& entity, Player* player, entityid_t attacker);
void on_entity_used(const Entity& entity, Player* player); void on_entity_used(const Entity& entity, Player* player);
/// @brief Called on UI view show /// @brief Called on UI document show
void on_ui_open(UiDocument* layout, std::vector<dv::value> args); void on_ui_open(const UiDocument& layout, std::vector<dv::value> args);
void on_ui_progress(UiDocument* layout, int workDone, int totalWork); void on_ui_progress(const UiDocument& layout, int workDone, int totalWork);
/// @brief Called on UI view close /// @brief Called on UI document close
void on_ui_close(UiDocument* layout, Inventory* inventory); void on_ui_close(const UiDocument& layout, Inventory* inventory);
/// @brief Called on UI document destroy
void on_ui_destroy(const UiDocument& layout);
/// @brief Called on Content loading /// @brief Called on Content loading
void on_scripts_loading(); void on_scripts_loading();

View file

@ -168,17 +168,127 @@ inline constexpr short MOUSE_KEYS_OFFSET = 1024;
static GLFWcursor* standard_cursors[static_cast<int>(CursorShape::LAST) + 1] = {}; static GLFWcursor* standard_cursors[static_cast<int>(CursorShape::LAST) + 1] = {};
class GLFWInput : public Input { class BaseInput : public Input {
public: public:
std::vector<uint> codepoints;
int scroll = 0; int scroll = 0;
void onKeyCallback(int key, bool pressed) {
bool prevPressed = keys[key];
keys[key] = pressed;
frames[key] = currentFrame;
if (pressed && !prevPressed) {
const auto& callbacks = keyCallbacks.find(static_cast<Keycode>(key));
if (callbacks != keyCallbacks.end()) {
callbacks->second.notify();
}
}
if (pressed && key < MOUSE_KEYS_OFFSET) {
pressedKeys.push_back(static_cast<Keycode>(key));
}
}
void onMouseCallback(int button, bool pressed) {
int key = button + MOUSE_KEYS_OFFSET;
onKeyCallback(key, pressed);
}
bool isCursorLocked() const override {
return cursorLocked;
}
void setCursorPosition(double xpos, double ypos) {
if (cursorDrag) {
delta.x += xpos - cursor.x;
delta.y += ypos - cursor.y;
} else {
cursorDrag = true;
}
cursor.x = xpos;
cursor.y = ypos;
}
Bindings& getBindings() override {
return bindings;
}
const Bindings& getBindings() const override {
return bindings;
}
ObserverHandler addKeyCallback(Keycode key, KeyCallback callback) override {
return keyCallbacks[key].add(std::move(callback));
}
const std::vector<Keycode>& getPressedKeys() const override {
return pressedKeys;
}
const std::vector<uint>& getCodepoints() const override {
return codepoints;
}
CursorState getCursor() const override {
return {isCursorLocked(), cursor, delta};
}
int getScroll() override {
return scroll;
}
bool pressed(Keycode key) const override {
int keycode = static_cast<int>(key);
if (keycode < 0 || keycode >= KEYS_BUFFER_SIZE) {
return false;
}
return keys[keycode];
}
bool jpressed(Keycode keycode) const override {
return pressed(keycode) &&
frames[static_cast<int>(keycode)] == currentFrame;
}
bool clicked(Mousecode code) const override {
return pressed(
static_cast<Keycode>(MOUSE_KEYS_OFFSET + static_cast<int>(code))
);
}
bool jclicked(Mousecode code) const override {
return clicked(code) &&
frames[static_cast<int>(code) + MOUSE_KEYS_OFFSET] ==
currentFrame;
}
void simulateKey(Keycode key, bool pressed) override {
onKeyCallback(static_cast<int>(key), pressed);
}
void simulateClick(int button, bool pressed) override {
onMouseCallback(static_cast<int>(button), pressed);
}
void simulateCursorPos(double xpos, double ypos) override {
setCursorPosition(xpos, ypos);
}
void simulateCodepoint(uint codepoint) override {
codepoints.push_back(codepoint);
}
protected:
uint currentFrame = 0; uint currentFrame = 0;
uint frames[KEYS_BUFFER_SIZE] {}; uint frames[KEYS_BUFFER_SIZE] {};
std::vector<uint> codepoints;
std::vector<Keycode> pressedKeys; std::vector<Keycode> pressedKeys;
Bindings bindings; Bindings bindings;
bool keys[KEYS_BUFFER_SIZE] {}; bool keys[KEYS_BUFFER_SIZE] {};
std::unordered_map<Keycode, util::HandlersList<>> keyCallbacks; std::unordered_map<Keycode, util::HandlersList<>> keyCallbacks;
bool cursorLocked = false;
bool cursorDrag = false;
glm::vec2 delta {};
glm::vec2 cursor {};
};
class GLFWInput : public BaseInput {
public:
GLFWInput(GLFWwindow* window) GLFWInput(GLFWwindow* window)
: window(window) { : window(window) {
} }
@ -228,26 +338,6 @@ public:
} }
} }
void onKeyCallback(int key, bool pressed) {
bool prevPressed = keys[key];
keys[key] = pressed;
frames[key] = currentFrame;
if (pressed && !prevPressed) {
const auto& callbacks = keyCallbacks.find(static_cast<Keycode>(key));
if (callbacks != keyCallbacks.end()) {
callbacks->second.notify();
}
}
if (pressed && key < MOUSE_KEYS_OFFSET) {
pressedKeys.push_back(static_cast<Keycode>(key));
}
}
void onMouseCallback(int button, bool pressed) {
int key = button + MOUSE_KEYS_OFFSET;
onKeyCallback(key, pressed);
}
const char* getClipboardText() const override { const char* getClipboardText() const override {
return glfwGetClipboardString(window); return glfwGetClipboardString(window);
} }
@ -256,41 +346,6 @@ public:
glfwSetClipboardString(window, text); glfwSetClipboardString(window, text);
} }
int getScroll() override {
return scroll;
}
bool pressed(Keycode key) const override {
int keycode = static_cast<int>(key);
if (keycode < 0 || keycode >= KEYS_BUFFER_SIZE) {
return false;
}
return keys[keycode];
}
bool jpressed(Keycode keycode) const override {
return pressed(keycode) &&
frames[static_cast<int>(keycode)] == currentFrame;
}
bool clicked(Mousecode code) const override {
return pressed(
static_cast<Keycode>(MOUSE_KEYS_OFFSET + static_cast<int>(code))
);
}
bool jclicked(Mousecode code) const override {
return clicked(code) &&
frames[static_cast<int>(code) + MOUSE_KEYS_OFFSET] ==
currentFrame;
}
CursorState getCursor() const override {
return {isCursorLocked(), cursor, delta};
}
bool isCursorLocked() const override {
return cursorLocked;
}
void toggleCursor() override { void toggleCursor() override {
cursorDrag = false; cursorDrag = false;
if (cursorLocked) { if (cursorLocked) {
@ -301,43 +356,8 @@ public:
} }
cursorLocked = !cursorLocked; cursorLocked = !cursorLocked;
} }
void setCursorPosition(double xpos, double ypos) {
if (cursorDrag) {
delta.x += xpos - cursor.x;
delta.y += ypos - cursor.y;
} else {
cursorDrag = true;
}
cursor.x = xpos;
cursor.y = ypos;
}
Bindings& getBindings() override {
return bindings;
}
const Bindings& getBindings() const override {
return bindings;
}
ObserverHandler addKeyCallback(Keycode key, KeyCallback callback) override {
return keyCallbacks[key].add(std::move(callback));
}
const std::vector<Keycode>& getPressedKeys() const override {
return pressedKeys;
}
const std::vector<uint>& getCodepoints() const override {
return codepoints;
}
private: private:
GLFWwindow* window; GLFWwindow* window;
bool cursorLocked = false;
bool cursorDrag = false;
glm::vec2 delta {};
glm::vec2 cursor {};
}; };
static_assert(!std::is_abstract<GLFWInput>()); static_assert(!std::is_abstract<GLFWInput>());

View file

@ -274,6 +274,11 @@ public:
virtual bool clicked(Mousecode mousecode) const = 0; virtual bool clicked(Mousecode mousecode) const = 0;
virtual bool jclicked(Mousecode mousecode) const = 0; virtual bool jclicked(Mousecode mousecode) const = 0;
virtual void simulateKey(Keycode key, bool pressed) = 0;
virtual void simulateClick(int button, bool pressed) = 0;
virtual void simulateCursorPos(double xpos, double ypos) = 0;
virtual void simulateCodepoint(uint codepoint) = 0;
virtual CursorState getCursor() const = 0; virtual CursorState getCursor() const = 0;
virtual bool isCursorLocked() const = 0; virtual bool isCursorLocked() const = 0;

View file

@ -16,4 +16,15 @@ TEST(io, memory_istream) {
ASSERT_EQ(text, std::string(data)); ASSERT_EQ(text, std::string(data));
stream.read(text.data(), 1); stream.read(text.data(), 1);
ASSERT_TRUE(stream.eof()); ASSERT_TRUE(stream.eof());
// seek
stream.clear();
stream.seekg(0);
ASSERT_TRUE(stream.good());
stream.read(text.data(), n);
ASSERT_EQ(text, std::string(data));
stream.seekg(-6, std::ios_base::cur);
ASSERT_TRUE(stream.good());
stream.read(text.data(), 6);
ASSERT_EQ(text.substr(0, 6), "world!");
} }