Merge branch 'main' into blocks-renderer-and-generator-update-review

This commit is contained in:
MihailRis 2025-11-22 22:51:01 +03:00
commit 1772f0b58c
131 changed files with 2202 additions and 489 deletions

View file

@ -3,7 +3,7 @@
## Latest release
- [Download](https://github.com/MihailRis/VoxelCore/releases/latest) | [Скачать](https://github.com/MihailRis/VoxelCore/releases/latest)
- [Documentation](https://github.com/MihailRis/VoxelCore/blob/release-0.28/doc/en/main-page.md) | [Документация](https://github.com/MihailRis/VoxelCore/blob/release-0.28/doc/ru/main-page.md)
- [Documentation](https://github.com/MihailRis/VoxelCore/blob/release-0.29/doc/en/main-page.md) | [Документация](https://github.com/MihailRis/VoxelCore/blob/release-0.29/doc/ru/main-page.md)
---

View file

@ -4,6 +4,7 @@ assert(file.exists("config:"))
debug.log("write text file")
assert(file.write("config:text.txt", "example, пример"))
assert(file.exists("config:text.txt"))
assert(file.isfile("config:text.txt"))
debug.log("read text file")
assert(file.read("config:text.txt") == "example, пример")
@ -18,11 +19,23 @@ assert(file.isdir("config:dir"))
debug.log("remove directory")
file.remove("config:dir")
assert(not file.isdir("config:dir"))
debug.log("create directories")
file.mkdirs("config:dir/subdir/other")
assert(file.isdir("config:dir/subdir/other"))
debug.log("list directory")
file.write("config:dir/subdir/a.txt", "helloworld")
file.write("config:dir/subdir/b.txt", "gfgsfhs")
local entries = file.list("config:dir/subdir")
assert(#entries == 3)
table.sort(entries)
assert(entries[1] == "config:dir/subdir/a.txt")
assert(entries[2] == "config:dir/subdir/b.txt")
assert(entries[3] == "config:dir/subdir/other")
debug.log("remove tree")
file.remove_tree("config:dir")
assert(not file.isdir("config:dir"))

View file

@ -0,0 +1,67 @@
app.create_memory_device("memtest")
local tmp = file.create_memory_device()
debug.log("check initial state")
assert(file.exists("memtest:"))
assert(file.is_writeable("memtest:"))
assert(file.is_writeable(tmp..":"))
debug.log("write text file")
assert(file.write("memtest:text.txt", "example, пример"))
assert(file.exists("memtest:text.txt"))
assert(file.isfile("memtest:text.txt"))
debug.log("read text file")
assert(file.read("memtest:text.txt") == "example, пример")
debug.log("delete file")
file.remove("memtest:text.txt")
assert(not file.exists("memtest:text.txt"))
debug.log("create directory")
file.mkdir("memtest:dir")
assert(file.isdir("memtest:dir"))
debug.log("remove directory")
file.remove("memtest:dir")
assert(not file.isdir("memtest:dir"))
debug.log("create directories")
file.mkdirs("memtest:dir/subdir/other")
assert(file.isdir("memtest:dir/subdir/other"))
debug.log("list directory")
file.write("memtest:dir/subdir/a.txt", "helloworld")
file.write("memtest:dir/subdir/b.txt", "gfgsfhs")
local entries = file.list("memtest:dir/subdir")
assert(#entries == 3)
table.sort(entries)
assert(entries[1] == "memtest:dir/subdir/a.txt")
assert(entries[2] == "memtest:dir/subdir/b.txt")
assert(entries[3] == "memtest:dir/subdir/other")
debug.log("remove tree")
file.remove_tree("memtest:dir")
assert(not file.isdir("memtest:dir"))
debug.log("write binary file")
local bytes = {0xDE, 0xAD, 0xC0, 0xDE}
file.write_bytes("memtest:binary", bytes)
assert(file.exists("memtest:binary"))
debug.log("write binary file")
local bytes = {0xDE, 0xAD, 0xC0, 0xDE}
file.write_bytes("memtest:binary", bytes)
assert(file.exists("memtest:binary"))
debug.log("read binary file")
local rbytes = file.read_bytes("memtest:binary")
assert(#rbytes == #bytes)
for i, b in ipairs(bytes) do
assert(rbytes[i] == b)
end
debug.log("delete file")
file.remove("memtest:binary")
assert(not file.exists("memtest:binary"))

View file

@ -1,8 +1,9 @@
# Documentation
Documentation for 0.30 (in development).
Documentation for 0.30.
[Documentation for 0.29.](https://github.com/MihailRis/voxelcore/blob/release-0.29/doc/ru/main-page.md)
> [!WARNING]
> Version is in development. Proceed to [Documentation for 0.29.](https://github.com/MihailRis/voxelcore/blob/release-0.29/doc/ru/main-page.md)
## Sections

View file

@ -158,3 +158,36 @@ app.get_setting_info(name: str) -> {
```
Returns a table with information about a setting. Throws an exception if the setting does not exist.
```lua
app.focus()
```
Brings the window to front and sets input focus.
```lua
app.create_memory_device(
-- entry-point name
name: str
)
```
Creates an in-memory filesystem.
```lua
app.get_content_sources() -> table<string>
```
Returns a list of content sources (paths), in descending priority order.
``lua
app.set_content_sources(sources: table<string>)
```
Sets a list of content sources (paths). Specified in descending priority order.
``lua
app.reset_content_sources()
```
Resets content sources.

View file

@ -139,6 +139,12 @@ file.create_zip(directory: str, output_file: str) --> str
Creates a ZIP archive from the contents of the specified directory.
```lua
file.create_memory_device() --> str
```
Creates a memory file system and returns entry point name. Lives until content unload.
```lua
file.name(path: str) --> str
```

View file

@ -13,7 +13,16 @@ input.mousecode(mousename: str) --> int
Returns mouse button code or -1 if unknown
```lua
input.add_callback(bindname: str, callback: function)
input.add_callback(
-- Binding name
bindname: str,
-- Handler
callback: function
-- UI element that owns the handler (responsible for the handler's lifetime)
[optional] owner: Element,
-- Ignore input capture by UI elements
[optional] istoplevel: bool
)
```
Add binding activation callback. Example:

View file

@ -57,7 +57,10 @@ network.tcp_connect(
-- Function called upon successful connection
-- Sending will not work before connection
-- Socket is passed as the only argument
callback: function(Socket)
callback: function(Socket),
-- Function called when a connection error occurs
-- Arguments passed: socket and error text
[optional] error_callback: function(Socket, str)
) --> Socket
```
@ -79,6 +82,16 @@ socket:recv(
-- Returns nil on error (socket is closed or does not exist).
-- If there is no data yet, returns an empty byte array.
-- Asynchronous version for use in coroutines.
-- Waits for the entire specified number of bytes to be received.
-- If socket closes, function works like socket:recv
socket:recv_async(
-- Size of the byte array to read
length: int,
-- Use table instead of Bytearray
[optional] usetable: bool=false
) -> nil|table|Bytearray
-- Closes the connection
socket:close()
@ -129,3 +142,10 @@ network.get_total_upload() --> int
-- in bytes.
network.get_total_download() --> int
```
## Other
```lua
-- Looks for a free port to use.
network.find_free_port() --> int or nil
```

View file

@ -40,13 +40,13 @@ Sets x, y, z player linear velocity
player.get_rot(playerid: int, interpolated: bool) -> number, number, number
```
Returns x, y, z of camera rotation (radians). Interpolation is relevant in cases where the rotation refresh rate is lower than the frame rate.
Returns x, y, z of camera rotation (degrees). Interpolation is relevant in cases where the rotation refresh rate is lower than the frame rate.
```lua
player.set_rot(playerid: int, x: number, y: number, z: number)
```
Set camera rotation (radians)
Set camera rotation (degrees)
```lua
player.get_inventory(playerid: int) -> int, int

View file

@ -4,6 +4,10 @@
Callbacks specified in block script.
> [!WARNING]
> events such as on_block_tick, on_block_present, and on_block_removed
> can cause performance issues if used carelessly or excessively.
```lua
function on_placed(x, y, z, playerid)
```
@ -53,6 +57,21 @@ function on_block_tick(x, y, z, tps: number)
Called tps (20 / tick-interval) times per second for a block.
Use 1/tps instead of `time.delta()`.
```lua
function on_block_present(x, y, z)
```
Called for a specific block when it appears in the world (generated/loaded/placed).
The call occurs within a time period that may depend on the event queue load.
Under light load, it occurs during the first tick interval of the block.
on_block_tick is not called until the event is called.
```lua
function on_block_removed(x, y, z)
```
Called when chunk containing the block unloads.
```lua
function on_player_tick(playerid: int, tps: int)
```

View file

@ -194,13 +194,23 @@ Here, *color* can be specified in the following ways:
| data:blit(src: Canvas, dst_x: int, dst_y: int) | draws the src canvas at the specified coordinates |
| data:clear() | clears the canvas |
| data:clear(*color*) | fills the canvas with the specified RGBA color |
| data:rect(x: int, y: int, w: int, h: int, *color*) | fills the rectangle with the specified RGBA color |
| data:update() | applies changes to the canvas and uploads it to the GPU |
| data:set_data(data: table<int>) | replaces pixel data (width * height * 4 numbers) |
| data:set_data(data: Bytearray | table<int>) | replaces pixel data (width * height * 4 numbers) |
| data:get_data() | creates a Bytearray object with the image's pixel data |
| data:create_texture(name: str) | creates and shares texture to renderer |
| data:unbind_texture() | unbinds the texture from the canvas |
| data:mul(*color* or Canvas) | multiplies a color by the specified color or canvas |
| data:add(*color* or Canvas) | adds a color or another canvas to a color |
| data:sub(*color* or Canvas) | subtracts a color or another canvas to a color |
| data:encode(format: str) | encodes image to specified format and returns bytearray |
To decode a byte array into a Canvas, use the static method:
```lua
Canvas.decode(data: Bytearray, format: str) -> Canvas
```
Currently, only png is supported.
## Inline frame (iframe)

View file

@ -136,6 +136,7 @@ The key code for comparison can be obtained via `input.keycode("key_name")`
- `text-wrap` - allows automatic text wrapping (works only with multiline: "true")
- `editable` - determines whether the text can be edited.
- `line-numbers` - enables line numbers display.
- `keep-line-selection` - keep showing selected line after defocus.
- `error-color` - color when entering incorrect data (the text does not pass the validator check). Type: RGBA color.
- `text-color` - text color. Type: RGBA color.
- `validator` - lua function that checks text for correctness. Takes a string as input, returns true if the text is correct.

View file

@ -14,7 +14,7 @@
Для создания команды консоли используется следующая функция:
```python
console.add_command(схема: str, исполнитель: function)
console.add_command(схема: str, исполнитель: function, чит_команда: bool)
```
Схема имеет следующий синтаксис:
@ -106,7 +106,7 @@ console.add_command(
"Teleport object",
function (args, kwargs)
player.set_pos(unpack(args))
end
end, true
)
```
@ -114,3 +114,23 @@ console.add_command(
- В kwargs передается таблица значений именованных аргументов.
Проверку и приведение типов интерпретатор команд производит автоматически.
## Остальные методы
```lua
console.set_cheat(name: String, cheat: Boolean) -> cheatIsChanged: Boolean
console.is_cheat(name: String) -> Boolean
```
Сеттер и геттер статуса "чит" команды
```lua
console.log(...) -- Выводит информацию во внутриигровую консоль
console.chat(...) -- Выводит информацию во внутриигровой чат
console.get_commands_list() -> Table<String> -- Возвращает таблицу со списком команд
console.get_command_info(name: String) -> Table -- Возвращает информацию о команде
console.execute(command: str) -- Выполняет команду
```

View file

@ -1,8 +1,9 @@
# Документация
Документация версии 0.30 (в разработке).
Документация версии 0.30.
[Документация 0.29.](https://github.com/MihailRis/voxelcore/blob/release-0.29/doc/ru/main-page.md)
> [!WARNING]
> Версия находится в разработке. Перейдите к [документации для 0.28](https://github.com/MihailRis/voxelcore/blob/release-0.29/doc/ru/main-page.md)
## Разделы

View file

@ -159,3 +159,35 @@ app.get_setting_info(name: str) -> {
```
Возвращает таблицу с информацией о настройке. Бросает исключение, если настройки не существует.
```lua
app.focus()
```
Переводит окно на передний план и устанавливает фокус ввода.
app.create_memory_device(
-- имя точки входа
name: str
)
```
Создаёт файловую систему в памяти.
```lua
app.get_content_sources() -> table<string>
```
Возвращает список источников контента (путей), в порядке убывания приоритета.
```lua
app.set_content_sources(sources: table<string>)
```
Устанавливает список источников контента (путей). Указывается в порядке убывания приоритета.
```lua
app.reset_content_sources()
```
Сбрасывает список источников контента.

View file

@ -139,6 +139,12 @@ file.create_zip(директория: str, выходной_файл: str) --> s
Создаёт ZIP-архив из содержимого указанной директории.
```lua
file.create_memory_device() --> str
```
Создаёт файловую систему в памяти, возвращает имя точки входа. Удаляется при выгрузке контента.
```lua
file.name(путь: str) --> str
```

View file

@ -13,7 +13,16 @@ input.mousecode(mousename: str) --> int
Возвращает код кнопки мыши по имени, либо -1
```lua
input.add_callback(bindname: str, callback: function)
input.add_callback(
-- Имя привязки
bindname: str,
-- Обработчик
callback: function
-- UI элемент-владелец обработчика (отвечает за срок жизни)
[опционально] owner: Element,
-- Игнорировать захват ввода UI элементами
[опционально] istoplevel: bool
)
```
Назначает функцию, которая будет вызываться при активации привязки. Пример:

View file

@ -58,6 +58,9 @@ network.tcp_connect(
-- До подключения отправка работать не будет
-- Как единственный аргумент передаётся сокет
callback: function(Socket)
-- Функция, вызываемая при ошибке подключения
-- Как аргументы передаются сокет и текст ошибки
[опционально] error_callback: function(Socket, str)
) --> Socket
```
@ -79,6 +82,16 @@ socket:recv(
-- В случае ошибки возвращает nil (сокет закрыт или несуществует).
-- Если данных пока нет, возвращает пустой массив байт.
-- Асинхронный вариант для использования в корутинах.
-- Ожидает получение всего указанного числа байт.
-- При закрытии сокета работает как socket:recv
socket:recv_async(
-- Размер читаемого массива байт
length: int,
-- Использовать таблицу вместо Bytearray
[опционально] usetable: bool=false
) -> nil|table|Bytearray
-- Закрывает соединение
socket:close()
@ -194,3 +207,10 @@ network.get_total_upload() --> int
-- в байтах.
network.get_total_download() --> int
```
## Другое
```lua
-- Ищет свободный для использования порт.
network.find_free_port() --> int или nil
```

View file

@ -40,13 +40,13 @@ player.set_vel(playerid: int, x: number, y: number, z: number)
player.get_rot(playerid: int, interpolated: bool=false) -> number, number, number
```
Возвращает x, y, z вращения камеры (в радианах). Интерполяция актуальна в случаях, когда частота обновления вращения ниже частоты кадров.
Возвращает x, y, z вращения камеры (в градусах). Интерполяция актуальна в случаях, когда частота обновления вращения ниже частоты кадров.
```lua
player.set_rot(playerid: int, x: number, y: number, z: number)
```
Устанавливает x, y вращения камеры (в радианах)
Устанавливает x, y вращения камеры (в градусах)
```lua
player.get_inventory(playerid: int) -> int, int

View file

@ -4,6 +4,10 @@
Функции для обработки событий, прописываемые в скрипте блока.
> [!WARNING]
> Mass events such as on_block_tick, on_block_present, and on_block_removed,
> if used carelessly or excessively, can lead to performance issues.
```lua
function on_placed(x, y, z, playerid)
```
@ -53,6 +57,21 @@ function on_block_tick(x, y, z, tps: number)
Вызывается tps (20 / tick-interval) раз в секунду для конкретного блока.
Используйте 1/tps вместо `time.delta()`.
```lua
function on_block_present(x, y, z)
```
Вызывается для конкретного блока при появлении (генерации/загрузке/размещении).
Вызов происходит в течение времени, которое может зависеть от нагрузки очереди событий.
При малой нагрузке происходит в течение первого такта блока (tick-interval).
До вызова события on_block_tick не вызывается.
```lua
function on_block_removed(x, y, z)
```
Вызывается при выгрузке чанка, в котором находится блок.
```lua
function on_player_tick(playerid: int, tps: int)
```

View file

@ -94,6 +94,13 @@ table.sub(arr: table, start: number | nil, stop: number | nil) -> table
Возвращает обрезанную версию таблицы с индекса **start** до индекса **stop** включительно, при этом пары ключ-значение не сохраняются в новой таблице. При значениях **nil** начинает с **1** и заканчивает **#arr** соответственно.
```lua
table.insert_unique(t: table, val: any)
table.insert_unique(t: table, pos: number, val: any)
```
Добавляет значение в таблицу, только если его там не было.
```lua
table.tostring(t: table) -> string
```

View file

@ -186,21 +186,31 @@ document["worlds-panel"]:clear()
- r: int, g: int, b: int
- r: int, g: int, b: int, a: int
| Метод | Описание |
|----------------------------------------------------------|------------------------------------------------------|
| data:at(x: int, y: int) | возвращает RGBA пиксель по указанным координатам |
| data:set(x: int, y: int, *цвет*) | изменяет RGBA пиксель по указанным координатам |
| data:line(x1: int, y1: int, x2: int, y2: int, *цвет*) | рисует линию с указанным RGBA цветом |
| data:blit(src: Canvas, dst_x: int, dst_y: int) | рисует src-холст на указанных координатах |
| data:clear() | очищает холст |
| data:clear(*цвет*) | заполняет холст указанным RGBA цветом |
| data:update() | применяет изменения и загружает холст в видеопамять |
| data:set_data(data: table<int>) | заменяет данные пикселей (ширина * высота * 4 чисел) |
| data:create_texture(name: str) | создаёт и делится текстурой с рендерером |
| data:unbind_texture() | отвязывает текстуру от холста |
| data:mul(*цвет* или Canvas) | умножает увет на указанный цвет или холст |
| data:add(*цвет* или Canvas) | прибавляет цвет или другой холст к цвету |
| data:sub(*цвет* или Canvas) | вычитает цвет или другой холст к цвету |
| Метод | Описание |
|----------------------------------------------------------|-----------------------------------------------------------------|
| data:at(x: int, y: int) | возвращает RGBA пиксель по указанным координатам |
| data:set(x: int, y: int, *цвет*) | изменяет RGBA пиксель по указанным координатам |
| data:line(x1: int, y1: int, x2: int, y2: int, *цвет*) | рисует линию с указанным RGBA цветом |
| data:blit(src: Canvas, dst_x: int, dst_y: int) | рисует src-холст на указанных координатах |
| data:clear() | очищает холст |
| data:clear(*цвет*) | заполняет холст указанным RGBA цветом |
| data:rect(x: int, y: int, w: int, h: int, *цвет*) | заполняет прямоугольник указанным RGBA цветом |
| data:update() | применяет изменения и загружает холст в видеопамять |
| data:set_data(data: bytearray | table<int>) | заменяет данные пикселей (ширина * высота * 4 чисел) |
| data:get_data() | создаёт объект Bytearray с пиксельными данными изображения |
| data:create_texture(name: str) | создаёт и делится текстурой с рендерером |
| data:unbind_texture() | отвязывает текстуру от холста |
| data:mul(*цвет* или Canvas) | умножает увет на указанный цвет или холст |
| data:add(*цвет* или Canvas) | прибавляет цвет или другой холст к цвету |
| data:sub(*цвет* или Canvas) | вычитает цвет или другой холст к цвету |
| data:encode(format: str) | кодирует изображение в указанный формат и возращает массив байт |
Для декодирования массива байт в Canvas используйте статический метод:
```lua
Canvas.decode(data: Bytearray, format: str) -> Canvas
```
На данный момент, из форматов поддерживается только png.
## Рамка встраивания (iframe)

View file

@ -137,6 +137,7 @@
- `text-wrap` - разрешает автоматический перенос текста (работает только при multiline: "true")
- `editable`- определяет возможность редактирования текста.
- `line-numbers` - включает отображение номеров строк.
- `keep-line-selection` - продолжать отображать выбранную строку при потере фокуса.
- `error-color` - цвет при вводе некорректных данных (текст не проходит проверку валидатора). Тип: RGBA цвет.
- `text-color` - цвет текста. Тип: RGBA цвет.
- `validator` - lua функция, проверяющая текст на корректность. Принимает на вход строку, возвращает true если текст корректен.

View file

@ -0,0 +1,153 @@
# VC-DBG protocol v1
## Notes
- '?' in name means that the attribute is optional.
## Connecting
### Step 1
Connection initiating with binary header exchange:
```
'v' 'c' '-' 'd' 'b' 'g' NUL XX
76 63 2D 64 62 67 00 XX
```
XX - protocol version number
Client sends header to the server. Then server responds.
Server closes connection after sending header if it mismatches.
### Step 2
Client sends 'connect' command.
## Messages
Message is:
- 32 bit little-endian unsigned integer - number of encoded message bytes.
- message itself (UTF-8 encoded json).
## Client-to-server
### Establishing connection
```json
{
"type": "connect",
"?disconnect-action": "resume|detach|terminate"
}
```
Configuring connection. Disconnect-action is action that debugged instance must perform on debugging client connection closed/refused.
- `resume` - Resume from pause mode and start listening for client.
- `detach` - Resume from pause mode and stop server.
- `terminate` - Stop the debugged application.
### Specific action signals.
```json
{
"type": "pause|resume|terminate|detach"
}
```
### Breakpoints management
```json
{
"type": "set-breakpoint|remove-breakpoint",
"source": "entry_point:path",
"line": 1
}
```
### Local value details request
```json
{
"type": "get-value",
"frame": 0,
"local": 1,
"path": ["path", "to", 1, "value"]
}
```
- `frame` - Call stack frame index (indexing from most recent call)
- `local` - Local variable index (based on `paused` event stack trace)
- `path` - Requsted value path segments. Example: `['a', 'b', 5]` is `local_variable.a.b[5]`
Responds with:
```json
{
"type": "value",
"frame": 0,
"local": 1,
"path": ["path", "to", 1, "value"],
"value": "value itself"
}
```
Example: actual value is table:
```lua
{a=5, b="test", 2={}}
```
Then `value` is:
```json
{
"a": {
"type": "number",
"short": "5"
},
"b": {
"type": "string",
"short": "test"
},
"1": {
"type": "table",
"short": "{...}"
}
}
```
## Server-to-client
### Response signals
```json
{
"type": "success|resumed"
}
```
### Pause event
```json
{
"type": "paused",
"?reason": "breakpoint|exception|step",
"?message": "...",
"?stack": [
{
"?function": "function name",
"source": "source name",
"what": "what",
"line": 1,
"locals": [
"name": {
"type": "local type",
"index": 1,
"short": "short value",
"size": 0
}
]
}
]
}
```

View file

@ -8,5 +8,5 @@ multiline-string-end = "]]"
keywords = [
"and", "break", "do", "else", "elseif", "end", "false", "for", "function",
"if", "in", "local", "nil", "not", "or", "repeat", "return", "then", "true",
"until", "while"
"until", "while", "self", "error"
]

View file

@ -1,5 +1,5 @@
function run_script(path)
__vc_start_app_script(path)
__vc_start_app_script(path, path)
end
function refresh()

View file

@ -29,7 +29,7 @@ local initialized = false
function on_open()
if not initialized then
initialized = true
local token = audio.input.__get_core_token()
local token = core.get_core_token()
document.root:add("<container id='tm' />")
local prev_amplitude = 0.0
document.tm:setInterval(16, function()
@ -54,16 +54,18 @@ function on_open()
create_setting("audio.volume-music", "Music", 0.01)
document.root:add("<label context='settings'>@Microphone</label>")
document.root:add("<select id='input_device_select' "..
"onselect='function(opt) audio.set_input_device(opt) end'/>")
"onselect='function(opt) core.set_setting(\"audio.input-device\", opt) end'/>")
document.root:add("<container id='input_volume_outer' color='#000000' size='4'>"
.."<container id='input_volume_inner' color='#00FF00FF' pos='1' size='2'/>"
.."</container>")
local selectbox = document.input_device_select
local devices = {}
local names = audio.get_input_devices_names()
local devices = {
{value="none", text=gui.str("None", "settings.microphone")},
}
local names = audio.__get_input_devices_names()
for i, name in ipairs(names) do
table.insert(devices, {value=name, text=name})
end
selectbox.options = devices
selectbox.value = audio.get_input_info().device_specifier
selectbox.value = audio.__get_input_info().device_specifier
end

View file

@ -60,7 +60,9 @@ if is_debugging then
current_func = _debug_getinfo(2).func
current_func_stack_size = calc_stack_size()
__pause("breakpoint")
debug.pull_events()
while debug.pull_events() do
__pause()
end
end, "lr")
end
@ -92,6 +94,7 @@ function debug.pull_events()
if not events then
return
end
local keepPaused = false
for i, event in ipairs(events) do
if event[1] == DBG_EVENT_SET_BREAKPOINT then
debug.set_breakpoint(event[2], event[3])
@ -116,9 +119,10 @@ function debug.pull_events()
value = value[key]
end
__sendvalue(value, event[2], event[3], event[4])
__pause()
keepPaused = true
end
end
return keepPaused
end
function debug.set_breakpoint(source, line)

View file

@ -167,6 +167,18 @@ function table.remove_value(t, x)
end
end
function table.insert_unique(t, pos_or_val, val)
if table.has(t, val or pos_or_val) then
return
end
if val then
table.insert(t, pos_or_val, val)
else
table.insert(t, pos_or_val)
end
end
function table.tostring(t)
local s = '['
for i,v in ipairs(t) do

View file

@ -3,7 +3,7 @@ local Schedule = {
set_interval = function(self, ms, callback, repetions)
local id = self._next_interval
self._intervals[id] = {
last_called = 0.0,
last_called = self._timer,
delay = ms / 1000.0,
callback = callback,
repetions = repetions,

View file

@ -39,6 +39,16 @@ end
local Socket = {__index={
send=function(self, ...) return network.__send(self.id, ...) end,
recv=function(self, ...) return network.__recv(self.id, ...) end,
recv_async=function(self, length, usetable)
while self:is_alive() do
local available = self:available()
if available >= length then
return self:recv(length, usetable)
end
coroutine.yield()
end
return self:recv(length, usetable)
end,
close=function(self) return network.__close(self.id) end,
available=function(self) return network.__available(self.id) or 0 end,
is_alive=function(self) return network.__is_alive(self.id) end,
@ -70,6 +80,7 @@ local DatagramServerSocket = {__index={
local _tcp_server_callbacks = {}
local _tcp_client_callbacks = {}
local _tcp_client_error_callbacks = {}
local _udp_server_callbacks = {}
local _udp_client_datagram_callbacks = {}
@ -116,10 +127,13 @@ network.tcp_open = function (port, handler)
return socket
end
network.tcp_connect = function(address, port, callback)
network.tcp_connect = function(address, port, callback, errorCallback)
local socket = setmetatable({id=0}, Socket)
socket.id = network.__connect_tcp(address, port)
_tcp_client_callbacks[socket.id] = function() callback(socket) end
if errorCallback then
_tcp_client_error_callbacks[socket.id] = function(message) errorCallback(socket, message) end
end
return socket
end
@ -167,78 +181,12 @@ local function clean(iterable, checkFun, ...)
end
end
local updating_blocks = {}
local TYPE_REGISTER = 0
local TYPE_UNREGISTER = 1
block.__perform_ticks = function(delta)
for id, entry in pairs(updating_blocks) do
entry.timer = entry.timer + delta
local steps = math.floor(entry.timer / entry.delta * #entry / 3)
if steps == 0 then
goto continue
end
entry.timer = 0.0
local event = entry.event
local tps = entry.tps
for i=1, steps do
local x = entry[entry.pointer + 1]
local y = entry[entry.pointer + 2]
local z = entry[entry.pointer + 3]
entry.pointer = (entry.pointer + 3) % #entry
events.emit(event, x, y, z, tps)
end
::continue::
end
end
block.__process_register_events = function()
local register_events = block.__pull_register_events()
if not register_events then
return
end
for i=1, #register_events, 4 do
local header = register_events[i]
local type = bit.band(header, 0xFFFF)
local id = bit.rshift(header, 16)
local x = register_events[i + 1]
local y = register_events[i + 2]
local z = register_events[i + 3]
local list = updating_blocks[id]
if type == TYPE_REGISTER then
if not list then
list = {}
list.event = block.name(id) .. ".blocktick"
list.tps = 20 / (block.properties[id]["tick-interval"] or 1)
list.delta = 1.0 / list.tps
list.timer = 0.0
list.pointer = 0
updating_blocks[id] = list
end
table.insert(list, x)
table.insert(list, y)
table.insert(list, z)
elseif type == TYPE_UNREGISTER then
if list then
for j=1, #list, 3 do
if list[j] == x and list[j + 1] == y and list[j + 2] == z then
for k=1,3 do
table.remove(list, j)
end
j = j - 3
end
end
end
end
end
end
network.__process_events = function()
local CLIENT_CONNECTED = 1
local CONNECTED_TO_SERVER = 2
local DATAGRAM = 3
local RESPONSE = 4
local CONNECTION_ERROR = 5
local ON_SERVER = 1
local ON_CLIENT = 2
@ -258,6 +206,11 @@ network.__process_events = function()
if callback then
callback()
end
elseif etype == CONNECTION_ERROR then
local callback = _tcp_client_error_callbacks[cid]
if callback then
callback(addr)
end
elseif etype == DATAGRAM then
if side == ON_CLIENT then
local callback = _udp_client_datagram_callbacks[cid]

View file

@ -0,0 +1,144 @@
local updating_blocks = {}
local present_queues = {}
local REGISTER_BIT = 0x1
local UPDATING_BIT = 0x2
local PRESENT_BIT = 0x4
local REMOVED_BIT = 0x8
block.__perform_ticks = function(delta)
for id, entry in pairs(updating_blocks) do
entry.timer = entry.timer + delta
local steps = math.floor(entry.timer / entry.delta * #entry / 3)
if steps == 0 then
goto continue
end
entry.timer = 0.0
local event = entry.event
local tps = entry.tps
for i=1, steps do
local x = entry[entry.pointer + 1]
local y = entry[entry.pointer + 2]
local z = entry[entry.pointer + 3]
entry.pointer = (entry.pointer + 3) % #entry
events.emit(event, x, y, z, tps)
end
::continue::
end
for id, queue in pairs(present_queues) do
queue.timer = queue.timer + delta
local steps = math.floor(queue.timer / queue.delta * #queue / 3)
if steps == 0 then
goto continue
end
queue.timer = 0.0
local event = queue.event
local update_list = updating_blocks[id]
for i=1, steps do
local index = #queue - 2
if index <= 0 then
break
end
local x = queue[index]
local y = queue[index + 1]
local z = queue[index + 2]
for j=1,3 do
table.remove(queue, index)
end
events.emit(event, x, y, z)
if queue.updating then
table.insert(update_list, x)
table.insert(update_list, y)
table.insert(update_list, z)
end
end
::continue::
end
end
local block_pull_register_events = block.__pull_register_events
block.__pull_register_events = nil
block.__process_register_events = function()
local register_events = block_pull_register_events()
if not register_events then
return
end
local emit_event = events.emit
local removed_events = {}
for i=1, #register_events, 4 do
local header = register_events[i]
local event_bits = bit.band(header, 0xFFFF)
local id = bit.rshift(header, 16)
local x = register_events[i + 1]
local y = register_events[i + 2]
local z = register_events[i + 3]
local is_register = bit.band(event_bits, REGISTER_BIT) ~= 0
local is_updating = bit.band(event_bits, UPDATING_BIT) ~= 0
local is_present = bit.band(event_bits, PRESENT_BIT) ~= 0
local is_removed = bit.band(event_bits, REMOVED_BIT) ~= 0
local list = updating_blocks[id]
if not is_register and is_removed then
local rm_event = removed_events[id]
if not rm_event then
rm_event = block.name(id) .. ".blockremoved"
removed_events[id] = rm_event
end
emit_event(rm_event, x, y, z)
end
if not list and is_register and is_updating then
list = {}
list.event = block.name(id) .. ".blocktick"
list.tps = 20 / (block.properties[id]["tick-interval"] or 1)
list.delta = 1.0 / list.tps
list.timer = 0.0
list.pointer = 0
updating_blocks[id] = list
end
if is_register and is_present then
local present_queue = present_queues[id]
if not present_queue then
present_queue = {}
present_queue.event = block.name(id) .. ".blockpresent"
present_queue.tps = 20 / (block.properties[id]["tick-interval"] or 1)
present_queue.delta = 1.0 / present_queue.tps
present_queue.timer = 0.0
present_queue.pointer = 0
present_queue.updating = is_updating
present_queues[id] = present_queue
end
table.insert(present_queue, x)
table.insert(present_queue, y)
table.insert(present_queue, z)
goto continue
end
if not is_updating then
goto continue
end
if is_register then
table.insert(list, x)
table.insert(list, y)
table.insert(list, z)
else
if not list then
goto continue
end
for j=1, #list, 3 do
if list[j] == x and list[j + 1] == y and list[j + 2] == z then
for k=1,3 do
table.remove(list, j)
end
j = j - 3
end
end
end
::continue::
end
end

View file

@ -93,7 +93,7 @@ console.add_command(
if entity then
entity.transform:set_pos({x, y, z})
end
end
end, true
)
console.add_command(
"echo value:str",
@ -108,7 +108,7 @@ console.add_command(
function(args, kwargs)
world.set_day_time(args[1])
return "Time set to " .. args[1]
end
end, true
)
console.add_command(
@ -123,7 +123,7 @@ console.add_command(
world.set_day_time_speed(1.0)
return "Daily cycle has started"
end
end
end, true
)
console.add_command(
@ -144,7 +144,7 @@ console.add_command(
local h = math.floor(math.abs(y2-y1+1) + 0.5)
local d = math.floor(math.abs(z2-z1+1) + 0.5)
return tostring(w * h * d) .. " blocks set"
end
end, true
)
console.add_command(
@ -154,7 +154,7 @@ console.add_command(
local eid = entities.spawn("base:player", {player.get_pos(args[1])}):get_uid()
player.set_entity(args[1], eid)
return "spawned new player entity #" .. tostring(eid)
end
end, true
)
@ -164,7 +164,7 @@ console.add_command(
function(args, kwargs)
local eid = entities.spawn(args[1], {args[2], args[3], args[4]})
return string.format("spawned %s at %s, %s, %s", unpack(args))
end
end, true
)
console.add_command(
@ -177,7 +177,7 @@ console.add_command(
entity:despawn()
return "despawned entity #" .. tostring(eid)
end
end
end, true
)
console.add_command(
@ -231,7 +231,7 @@ console.add_command(
local rotation = args[5]
local fragment = generation.load_fragment(filename)
fragment:place({x, y, z}, rotation)
end
end, true
)
console.add_command(
@ -242,7 +242,7 @@ console.add_command(
local value = args[2]
rules.set(name, value)
return "rule '"..name.."' set to "..tostring(value)
end
end, true
)
console.add_command(
@ -285,7 +285,7 @@ console.add_command(
local preset = json.parse(file.read(filename))
gfx.weather.change(preset, args[2], args[1])
return "weather set to "..filename.." preset ("..tostring(args[2]).." s)"
end
end, true
)
console.add_command(
@ -313,14 +313,3 @@ console.add_command(
return "available presets:" .. presets
end
)
console.cheats = {
"blocks.fill",
"tp",
"fragment.place",
"time.set",
"time.daycycle",
"entity.despawn",
"player.respawn",
"weather.set",
}

View file

@ -47,6 +47,7 @@ local function complete_app_lib(app)
end
app.reset_content = core.reset_content
app.is_content_loaded = core.is_content_loaded
app.set_title = core.set_title
function app.config_packs(packs_list)
-- Check if packs are valid and add dependencies to the configuration
@ -116,7 +117,7 @@ function pack.unload(prefix)
events.remove_by_prefix(prefix)
end
function __vc_start_app_script(path)
function __vc_start_app_script(path, name)
debug.log("starting application script "..path)
local code = file.read(path)
@ -126,7 +127,11 @@ function __vc_start_app_script(path)
end
local script_env = setmetatable({app = app or __vc_app}, {__index=_G})
chunk = setfenv(chunk, script_env)
return __vc_start_coroutine(chunk, path)
if name then
return start_coroutine(chunk, name)
else
return __vc_start_coroutine(chunk)
end
end
gui_util = require "core:internal/gui_util"
@ -166,6 +171,42 @@ function console.log(...)
log_element:paste(text)
end
local console_add_command = console.__add_command
console.__add_command = nil
function console.add_command(scheme, description, handler, is_cheat)
console_add_command(scheme, description, handler)
if not is_cheat then return end
local name = string.match(scheme, "^(%S+)")
if not name then
error("Incorrect command syntax, command name not found")
end
table.insert_unique(console.cheats, name)
end
function console.is_cheat(name)
if not table.has(console.get_commands_list(), name) then
error(string.format("command \"%s\" not found", name))
end
return table.has(console.cheats, name)
end
function console.set_cheat(name, status)
local is_cheat = console.is_cheat(name)
if status and not is_cheat then
table.insert(console.cheats, name)
return true
elseif not status and is_cheat then
table.remove_value(console.cheats, name)
return true
end
return false
end
function console.chat(...)
console.log(...)
events.emit("core:chat", ...)
@ -357,6 +398,10 @@ function __vc_on_world_tick(tps)
time.schedules.world:tick(1.0 / tps)
end
function __vc_process_before_quit()
block.__process_register_events()
end
function __vc_on_world_save()
local rule_values = {}
for name, rule in pairs(rules.rules) do
@ -436,6 +481,7 @@ local __post_runnables = {}
local fn_audio_reset_fetch_buffer = audio.__reset_fetch_buffer
audio.__reset_fetch_buffer = nil
core.get_core_token = audio.input.__get_core_token
function __process_post_runnables()
if #__post_runnables then

View file

@ -185,7 +185,7 @@ local internal_locked = false
-- Example `base:scripts/tests.lua`
--
-- nocache - ignore cached script, load anyway
local function __load_script(path, nocache, env)
function __load_script(path, nocache, env)
local packname, filename = parse_path(path)
if internal_locked and (packname == "res" or packname == "core")

View file

@ -109,6 +109,7 @@ settings.Conflict=Найдены возможные конфликты
settings.Windowed=Оконный
settings.Borderless=Безрамочный
settings.Microphone=Микрофон
settings.microphone.None=Нет
# Управление
chunks.reload=Перезагрузить Чанки

View file

@ -112,7 +112,7 @@ target_compile_options(
-Wno-sign-compare
-Wno-unknown-pragmas
>
$<$<CXX_COMPILER_ID:Clang>:
$<$<CXX_COMPILER_ID:GNU>:
-Wduplicated-branches
-Wduplicated-cond
>)

View file

@ -74,8 +74,7 @@ ALInputDevice::ALInputDevice(
uint bitsPerSample,
uint sampleRate
)
: al(al),
device(device),
: device(device),
channels(channels),
bitsPerSample(bitsPerSample),
sampleRate(sampleRate) {

View file

@ -108,7 +108,6 @@ namespace audio {
size_t read(char* buffer, size_t bufferSize) override;
private:
ALAudio* al;
ALCdevice* device;
uint channels;
uint bitsPerSample;

View file

@ -24,12 +24,9 @@ void MemoryPCMStream::close() {
}
size_t MemoryPCMStream::read(char* dst, size_t bufferSize) {
if (!open) {
if (!open || buffer.empty()) {
return PCMStream::ERROR;
}
if (buffer.empty()) {
return 0;
}
size_t count = std::min<size_t>(bufferSize, buffer.size());
std::memcpy(dst, buffer.data(), count);
buffer.erase(buffer.begin(), buffer.begin() + count);

View file

@ -23,6 +23,7 @@ namespace {
std::unordered_map<speakerid_t, std::shared_ptr<Stream>> streams;
std::vector<std::unique_ptr<Channel>> channels;
util::ObjectsKeeper objects_keeper {};
std::unique_ptr<InputDevice> input_device = nullptr;
}
Channel::Channel(std::string name) : name(std::move(name)) {
@ -151,8 +152,6 @@ public:
}
};
static std::unique_ptr<InputDevice> input_device = nullptr;
void audio::initialize(bool enabled, AudioSettings& settings) {
enabled = enabled && settings.enabled.get();
if (enabled) {
@ -183,9 +182,9 @@ void audio::initialize(bool enabled, AudioSettings& settings) {
}, true));
}
input_device = backend->openInputDevice("", 44100, 1, 16);
if (input_device) {
input_device->startCapture();
::input_device = backend->openInputDevice("", 44100, 1, 16);
if (::input_device) {
::input_device->startCapture();
}
}
@ -270,18 +269,26 @@ std::vector<std::string> audio::get_output_devices_names() {
}
void audio::set_input_device(const std::string& deviceName) {
logger.info() << "setting input device to " << deviceName;
if (deviceName == audio::DEVICE_NONE) {
if (::input_device) {
::input_device->stopCapture();
}
::input_device = nullptr;
return;
}
auto newDevice = backend->openInputDevice(deviceName, 44100, 1, 16);
if (newDevice == nullptr) {
logger.error() << "could not open input device: " << deviceName;
return;
}
if (input_device) {
input_device->stopCapture();
if (::input_device) {
::input_device->stopCapture();
}
input_device = std::move(newDevice);
if (input_device) {
input_device->startCapture();
::input_device = std::move(newDevice);
if (::input_device) {
::input_device->startCapture();
}
}

View file

@ -26,6 +26,8 @@ namespace audio {
constexpr inline size_t MAX_INPUT_SAMPLES = 22050;
inline std::string_view DEVICE_NONE = "none";
class Speaker;
/// @brief Audio speaker states

View file

@ -1,3 +1,4 @@
#define VC_ENABLE_REFLECTION
#include "imageio.hpp"
#include <functional>
@ -7,28 +8,34 @@
#include "io/io.hpp"
#include "png.hpp"
using namespace imageio;
using image_reader =
std::function<std::unique_ptr<ImageData>(const ubyte*, size_t)>;
using image_writer = std::function<void(const std::string&, const ImageData*)>;
static std::unordered_map<std::string, image_reader> readers {
{".png", png::load_image},
static std::unordered_map<ImageFileFormat, image_reader> readers {
{ImageFileFormat::PNG, png::load_image},
};
static std::unordered_map<std::string, image_writer> writers {
{".png", png::write_image},
static std::unordered_map<ImageFileFormat, image_writer> writers {
{ImageFileFormat::PNG, png::write_image},
};
bool imageio::is_read_supported(const std::string& extension) {
return readers.find(extension) != readers.end();
return extension == ".png";
}
bool imageio::is_write_supported(const std::string& extension) {
return writers.find(extension) != writers.end();
return extension == ".png";
}
std::unique_ptr<ImageData> imageio::read(const io::path& file) {
auto found = readers.find(file.extension());
ImageFileFormat format;
if (!ImageFileFormatMeta.getItem(file.extension().substr(1), format)) {
throw std::runtime_error("unsupported image format");
}
auto found = readers.find(format);
if (found == readers.end()) {
throw std::runtime_error(
"file format is not supported (read): " + file.string()
@ -44,8 +51,25 @@ std::unique_ptr<ImageData> imageio::read(const io::path& file) {
}
}
std::unique_ptr<ImageData> imageio::decode(
ImageFileFormat format, util::span<ubyte> src
) {
auto found = readers.find(format);
try {
return std::unique_ptr<ImageData>(found->second(src.data(), src.size()));
} catch (const std::runtime_error& err) {
throw std::runtime_error(
"could not to decode image: " + std::string(err.what())
);
}
}
void imageio::write(const io::path& file, const ImageData* image) {
auto found = writers.find(file.extension());
ImageFileFormat format;
if (!ImageFileFormatMeta.getItem(file.extension().substr(1), format)) {
throw std::runtime_error("unsupported image format");
}
auto found = writers.find(format);
if (found == writers.end()) {
throw std::runtime_error(
"file format is not supported (write): " + file.string()
@ -53,3 +77,14 @@ void imageio::write(const io::path& file, const ImageData* image) {
}
return found->second(io::resolve(file).u8string(), image);
}
util::Buffer<unsigned char> imageio::encode(
ImageFileFormat format, const ImageData& image
) {
switch (format) {
case ImageFileFormat::PNG:
return png::encode_image(image);
default:
throw std::runtime_error("file format is not supported for encoding");
}
}

View file

@ -4,10 +4,22 @@
#include <string>
#include "io/fwd.hpp"
#include "util/Buffer.hpp"
#include "util/EnumMetadata.hpp"
#include "util/span.hpp"
#include "typedefs.hpp"
class ImageData;
namespace imageio {
enum class ImageFileFormat {
PNG
};
VC_ENUM_METADATA(ImageFileFormat)
{"png", ImageFileFormat::PNG},
VC_ENUM_END
inline const std::string PNG = ".png";
bool is_read_supported(const std::string& extension);
@ -15,4 +27,6 @@ namespace imageio {
std::unique_ptr<ImageData> read(const io::path& file);
void write(const io::path& file, const ImageData* image);
std::unique_ptr<ImageData> decode(ImageFileFormat format, util::span<ubyte> src);
util::Buffer<unsigned char> encode(ImageFileFormat format, const ImageData& image);
}

View file

@ -11,6 +11,60 @@
static debug::Logger logger("png-coder");
static util::Buffer<ubyte> write_to_memory(uint width, uint height, const ubyte* data, bool alpha) {
uint pixsize = alpha ? 4 : 3;
std::vector<ubyte> buffer;
png_structp png_ptr = png_create_write_struct(
PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr
);
png_infop info_ptr = png_create_info_struct(png_ptr);
png_set_write_fn(
png_ptr,
&buffer,
[](png_structp pngPtr, png_bytep data, png_size_t length) {
auto& buf = *reinterpret_cast<std::vector<ubyte>*>(png_get_io_ptr(pngPtr));
buf.insert(
buf.end(),
reinterpret_cast<ubyte*>(data),
reinterpret_cast<ubyte*>(data) + length
);
},
nullptr
);
png_set_IHDR(
png_ptr,
info_ptr,
width,
height,
8,
alpha ? PNG_COLOR_TYPE_RGBA : PNG_COLOR_TYPE_RGB,
PNG_INTERLACE_NONE,
PNG_COMPRESSION_TYPE_BASE,
PNG_FILTER_TYPE_BASE
);
png_write_info(png_ptr, info_ptr);
auto row = std::make_unique<png_byte[]>(pixsize * width);
for (uint y = 0; y < height; y++) {
for (uint x = 0; x < width; x++) {
for (uint i = 0; i < pixsize; i++) {
row[x * pixsize + i] =
(png_byte)data[(y * width + x) * pixsize + i];
}
}
png_write_row(png_ptr, row.get());
}
png_write_end(png_ptr, nullptr);
png_free_data(png_ptr, info_ptr, PNG_FREE_ALL, -1);
png_destroy_write_struct(&png_ptr, &info_ptr);
return util::Buffer<ubyte>(buffer.data(), buffer.size());
}
// returns 0 if all-right, 1 otherwise
static int png_write(
const char* filename, uint width, uint height, const ubyte* data, bool alpha
@ -230,3 +284,13 @@ void png::write_image(const std::string& filename, const ImageData* image) {
image->getFormat() == ImageFormat::rgba8888
);
}
util::Buffer<ubyte> png::encode_image(const ImageData& image) {
auto format = image.getFormat();
return write_to_memory(
image.getWidth(),
image.getHeight(),
image.getData(),
format == ImageFormat::rgba8888
);
}

View file

@ -4,6 +4,7 @@
#include <string>
#include "typedefs.hpp"
#include "util/Buffer.hpp"
class Texture;
class ImageData;
@ -11,6 +12,7 @@ class ImageData;
namespace png {
std::unique_ptr<ImageData> load_image(const ubyte* bytes, size_t size);
void write_image(const std::string& filename, const ImageData* image);
util::Buffer<ubyte> encode_image(const ImageData& image);
std::unique_ptr<Texture> load_texture(const ubyte* bytes, size_t size);
std::unique_ptr<Texture> load_texture(const std::string& filename);
}

View file

@ -16,6 +16,13 @@ static void load_configs(Input& input, const io::path& root) {
auto configFolder = root / "config";
}
static std::vector<io::path> default_content_sources {
"world:content",
"user:content",
"project:content",
"res:content",
};
ContentControl::ContentControl(
const Project& project,
EnginePaths& paths,
@ -27,12 +34,7 @@ ContentControl::ContentControl(
postContent(std::move(postContent)),
basePacks(project.basePacks),
manager(std::make_unique<PacksManager>()) {
manager->setSources({
"world:content",
"user:content",
"project:content",
"res:content",
});
manager->setSources(default_content_sources);
}
ContentControl::~ContentControl() = default;
@ -68,6 +70,7 @@ void ContentControl::resetContent(const std::vector<std::string>& nonReset) {
scripting::on_content_reset();
setContentPacksRaw(manager->getAll(basePacks));
resetContentSources();
postContent();
}
@ -139,3 +142,15 @@ PacksManager& ContentControl::scan() {
manager->scan();
return *manager;
}
void ContentControl::setContentSources(std::vector<io::path> sources) {
manager->setSources(std::move(sources));
}
void ContentControl::resetContentSources() {
manager->setSources(default_content_sources);
}
const std::vector<io::path>& ContentControl::getContentSources() const {
return manager->getSources();
}

View file

@ -5,6 +5,7 @@
#include <string>
#include <functional>
#include "io/path.hpp"
#include "ContentPack.hpp"
class Content;
@ -13,10 +14,6 @@ class EnginePaths;
class Input;
struct Project;
namespace io {
class path;
}
class ContentControl {
public:
ContentControl(
@ -46,6 +43,10 @@ public:
const std::vector<ContentPack>& getAllContentPacks() const;
PacksManager& scan();
void setContentSources(std::vector<io::path> sources);
void resetContentSources();
const std::vector<io::path>& getContentSources() const;
private:
EnginePaths& paths;
Input& input;

View file

@ -9,6 +9,10 @@
PacksManager::PacksManager() = default;
const std::vector<io::path>& PacksManager::getSources() const {
return sources;
}
void PacksManager::setSources(std::vector<io::path> sources) {
this->sources = std::move(sources);
}

View file

@ -12,6 +12,9 @@ class PacksManager {
public:
PacksManager();
/// @brief Get current content packs sources
const std::vector<io::path>& getSources() const;
/// @brief Set content packs sources (search folders)
void setSources(std::vector<io::path> sources);

View file

@ -197,6 +197,7 @@ bool DebuggingServer::performCommand(
connectionEstablished = true;
logger.info() << "client connection established";
connection->sendResponse("success");
return true;
}
if (!connectionEstablished) {
return false;

View file

@ -70,6 +70,7 @@ void Engine::onContentLoad() {
for (auto& pack : content->getAllContentPacks()) {
auto configFolder = pack.folder / "config";
auto bindsFile = configFolder / "bindings.toml";
logger.info() << "loading bindings: " << bindsFile.string();
if (io::is_regular_file(bindsFile)) {
input->getBindings().read(
toml::parse(
@ -174,6 +175,10 @@ void Engine::initialize(CoreParameters coreParameters) {
langs::setup(lang, paths->resPaths.collectRoots());
}, true));
keepAlive(settings.audio.inputDevice.observe([](auto name) {
audio::set_input_device(name == "auto" ? "" : name);
}, true));
project->loadProjectStartScript();
if (!params.headless) {
project->loadProjectClientScript();

View file

@ -2,6 +2,7 @@
#include "debug/Logger.hpp"
#include "io/devices/StdfsDevice.hpp"
#include "io/devices/MemoryDevice.hpp"
#include "io/devices/ZipFileDevice.hpp"
#include "maths/util.hpp"
#include "typedefs.hpp"
@ -71,6 +72,14 @@ EnginePaths::EnginePaths(CoreParameters& params)
io::create_subdevice("config", "user", "config");
}
std::filesystem::path EnginePaths::getResourcesFolder() const {
return resourcesFolder;
}
std::filesystem::path EnginePaths::getUserFilesFolder() const {
return userFilesFolder;
}
io::path EnginePaths::getNewScreenshotFile(const std::string& ext) const {
auto folder = SCREENSHOTS_FOLDER;
if (!io::is_directory(folder)) {
@ -168,6 +177,18 @@ void EnginePaths::unmount(const std::string& name) {
mounted.erase(found);
}
std::string EnginePaths::createMemoryDevice() {
auto device = std::make_unique<io::MemoryDevice>();
std::string name;
do {
name = std::string("W.") + generate_random_base64<6>();
} while (std::find(mounted.begin(), mounted.end(), name) != mounted.end());
io::set_device(name, std::move(device));
mounted.push_back(name);
return name;
}
std::string EnginePaths::createWriteableDevice(const std::string& name) {
const auto& found = writeables.find(name);
if (found != writeables.end()) {

View file

@ -48,6 +48,9 @@ public:
EnginePaths(CoreParameters& params);
std::filesystem::path getResourcesFolder() const;
std::filesystem::path getUserFilesFolder() const;
io::path getWorldFolderByName(const std::string& name);
io::path getWorldsFolder() const;
@ -58,6 +61,7 @@ public:
void unmount(const std::string& name);
std::string createWriteableDevice(const std::string& name);
std::string createMemoryDevice();
void setEntryPoints(std::vector<PathsRoot> entryPoints);

View file

@ -33,11 +33,12 @@ WindowControl::Result WindowControl::initialize() {
auto& settings = engine.getSettings();
std::string title = project.title;
if (title.empty()) {
title = "VoxelCore v" +
std::to_string(ENGINE_VERSION_MAJOR) + "." +
std::to_string(ENGINE_VERSION_MINOR);
if (!title.empty()) {
title += " - ";
}
title += "VoxelCore v" +
std::to_string(ENGINE_VERSION_MAJOR) + "." +
std::to_string(ENGINE_VERSION_MINOR);
if (ENGINE_DEBUG_BUILD) {
title += " [debug]";
}

View file

@ -87,7 +87,7 @@ std::shared_ptr<UINode> create_debug_panel(
fpsMax = fps;
});
panel->listenInterval(1.0f, [&engine, &gui]() {
panel->listenInterval(1.0f, [&engine]() {
const auto& network = engine.getNetwork();
size_t totalDownload = network.getTotalDownload();
size_t totalUpload = network.getTotalUpload();

View file

@ -192,7 +192,7 @@ Hud::Hud(Engine& engine, LevelFrontend& frontend, Player& player)
gui.add(contentAccessPanel);
auto dplotter = std::make_shared<Plotter>(gui, 350, 250, 2000, 16);
dplotter->setGravity(Gravity::bottom_right);
dplotter->setGravity(Gravity::BOTTOM_RIGHT);
dplotter->setInteractive(false);
add(HudElement(HudElementMode::PERMANENT, nullptr, dplotter, true));

View file

@ -94,7 +94,11 @@ LevelScreen::LevelScreen(
renderer->clear();
return false;
}));
controller->preQuitCallbacks.listen([this]() {
if (!controller->getLevel()->getWorld()->isNameless()) {
saveWorldPreview();
}
});
animator = std::make_unique<TextureAnimator>();
animator->addAnimations(assets.getAnimations());
@ -104,11 +108,11 @@ LevelScreen::LevelScreen(
LevelScreen::~LevelScreen() {
if (!controller->getLevel()->getWorld()->isNameless()) {
saveDecorations();
saveWorldPreview();
}
scripting::on_frontend_close();
// unblock all bindings
input.getBindings().enableAll();
playerController->getPlayer()->chunks->saveAndClear();
controller->onWorldQuit();
engine.getPaths().setCurrentWorldFolder("");
}
@ -278,5 +282,6 @@ void LevelScreen::onEngineShutdown() {
if (hud->isInventoryOpen()) {
hud->closeInventory();
}
controller->processBeforeQuit();
controller->saveWorld();
}

View file

@ -16,7 +16,7 @@ ImageData::ImageData(ImageFormat format, uint width, uint height)
default:
throw std::runtime_error("format is not supported");
}
data = std::make_unique<ubyte[]>(width * height * pixsize);
data = std::make_unique<ubyte[]>((width + width % 2) * (height + width % 2) * pixsize);
}
ImageData::ImageData(ImageFormat format, uint width, uint height, std::unique_ptr<ubyte[]> data)
@ -470,6 +470,32 @@ void ImageData::addColor(const ImageData& other, int multiplier) {
}
}
void ImageData::extend(int newWidth, int newHeight) {
size_t comps;
switch (format) {
case ImageFormat::rgb888: comps = 3; break;
case ImageFormat::rgba8888: comps = 4; break;
default:
throw std::runtime_error("only unsigned byte formats supported");
}
auto newData = std::make_unique<ubyte[]>(newWidth * newHeight * comps);
for (uint y = 0; y < newHeight; y++) {
for (uint x = 0; x < newWidth; x++) {
for (size_t c = 0; c < comps; c++) {
if (x < width && y < height) {
newData[(y * newWidth + x) * comps + c] =
data[(y * width + x) * comps + c];
} else {
newData[(y * newWidth + x) * comps + c] = 0;
}
}
}
}
data = std::move(newData);
width = newWidth;
height = newHeight;
}
void ImageData::addColor(const glm::ivec4& color, int multiplier) {
uint comps;
switch (format) {

View file

@ -36,6 +36,7 @@ public:
void mulColor(const ImageData& other);
void addColor(const glm::ivec4& color, int multiplier);
void addColor(const ImageData& other, int multiplier);
void extend(int newWidth, int newHeight);
std::unique_ptr<ImageData> cropped(int x, int y, int width, int height) const;

View file

@ -44,10 +44,10 @@ void Texture::unbind() const {
void Texture::reload(const ImageData& image) {
width = image.getWidth();
height = image.getHeight();
reload(image.getData());
reload(image.getData(), width, height);
}
void Texture::reload(const ubyte* data) {
void Texture::reload(const ubyte* data, uint width, uint height) {
glBindTexture(GL_TEXTURE_2D, id);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0,
GL_RGBA, GL_UNSIGNED_BYTE, static_cast<const GLvoid*>(data));

View file

@ -18,7 +18,7 @@ public:
virtual void bind() const;
virtual void unbind() const;
void reload(const ubyte* data);
void reload(const ubyte* data, uint w, uint h);
void reloadPartial(const ImageData& image, uint x, uint y, uint w, uint h);
void setNearestFilter();

View file

@ -30,14 +30,14 @@ namespace gui {
glm::vec2 size,
glm::vec4 padding = glm::vec4(0.0f),
float interval = 2.0f,
Orientation orientation = Orientation::vertical
Orientation orientation = Orientation::VERTICAL
)
: Container(gui, std::move(size)),
padding(std::move(padding)),
interval(interval) {
}
Orientation orientation = Orientation::vertical;
Orientation orientation = Orientation::VERTICAL;
glm::vec4 padding;
float interval = 2.0f;
};

View file

@ -31,7 +31,7 @@ Button::Button(
GUI& gui,
const std::wstring& text,
glm::vec4 padding,
const onaction& action,
const OnAction& action,
glm::vec2 size
)
: Panel(gui, size, padding, 0.0f) {
@ -42,12 +42,12 @@ Button::Button(
}
if (action) {
listenAction(action);
listenClick(action);
}
setScrollable(false);
label = std::make_shared<Label>(gui, text);
label->setAlign(Align::center);
label->setAlign(Align::CENTER);
label->setSize(getContentSize());
label->setInteractive(false);
add(label);
@ -95,5 +95,5 @@ Align Button::getTextAlign() const {
if (label) {
return label->getAlign();
}
return Align::left;
return Align::LEFT;
}

View file

@ -19,7 +19,7 @@ namespace gui {
GUI& gui,
const std::wstring& text,
glm::vec4 padding,
const onaction& action,
const OnAction& action,
glm::vec2 size = glm::vec2(-1)
);

View file

@ -4,11 +4,11 @@
#include "graphics/core/DrawContext.hpp"
#include "graphics/core/Texture.hpp"
gui::Canvas::Canvas(GUI& gui, ImageFormat inFormat, glm::uvec2 inSize)
: UINode(gui, inSize) {
auto data = std::make_shared<ImageData>(inFormat, inSize.x, inSize.y);
mTexture = Texture::from(data.get());
mData = std::move(data);
gui::Canvas::Canvas(GUI& gui, ImageFormat format, glm::uvec2 size)
: UINode(gui, size) {
auto data = std::make_shared<ImageData>(format, size.x, size.y);
texture = Texture::from(data.get());
this->data = std::move(data);
}
void gui::Canvas::draw(const DrawContext& pctx, const Assets& assets) {
@ -16,6 +16,12 @@ void gui::Canvas::draw(const DrawContext& pctx, const Assets& assets) {
auto col = calcColor();
auto batch = pctx.getBatch2D();
batch->texture(mTexture.get());
batch->texture(texture.get());
batch->rect(pos.x, pos.y, size.x, size.y, 0, 0, 0, {}, false, false, col);
}
void gui::Canvas::setSize(const glm::vec2& size) {
UINode::setSize(size);
data->extend(size.x, size.y);
texture->reload(*data);
}

View file

@ -9,21 +9,23 @@ class Texture;
namespace gui {
class Canvas final : public UINode {
public:
explicit Canvas(GUI& gui, ImageFormat inFormat, glm::uvec2 inSize);
explicit Canvas(GUI& gui, ImageFormat format, glm::uvec2 size);
~Canvas() override = default;
void draw(const DrawContext& pctx, const Assets& assets) override;
[[nodiscard]] auto texture() const {
return mTexture;
void setSize(const glm::vec2& size) override;
[[nodiscard]] auto getTexture() const {
return texture;
}
[[nodiscard]] auto data() const {
return mData;
[[nodiscard]] auto getData() const {
return data;
}
private:
std::shared_ptr<::Texture> mTexture;
std::shared_ptr<ImageData> mData;
std::shared_ptr<::Texture> texture;
std::shared_ptr<ImageData> data;
};
}

View file

@ -52,7 +52,7 @@ FullCheckBox::FullCheckBox(
checkbox(std::make_shared<CheckBox>(gui, checked)),
label(std::make_shared<Label>(gui, text)) {
setColor(glm::vec4(0.0f));
setOrientation(Orientation::horizontal);
setOrientation(Orientation::HORIZONTAL);
add(checkbox);

View file

@ -64,7 +64,7 @@ namespace gui {
checkbox->setTooltip(text);
}
virtual void setSize(glm::vec2 new_size) override {
virtual void setSize(const glm::vec2& new_size) override {
Panel::setSize(new_size);
checkbox->setSize(glm::vec2(size.y, size.y));
}

View file

@ -93,10 +93,9 @@ void Container::act(float delta) {
}
}
}
GUI& gui = this->gui;
intervalEvents.erase(std::remove_if(
intervalEvents.begin(), intervalEvents.end(),
[&gui](const IntervalEvent& event) {
[](const IntervalEvent& event) {
return event.repeat == 0;
}
), intervalEvents.end());
@ -177,6 +176,7 @@ void Container::add(const std::shared_ptr<UINode>& node) {
parent->setMustRefresh();
parent = parent->getParent();
}
gui.getWindow().setShouldRefresh();
}
void Container::add(const std::shared_ptr<UINode>& node, glm::vec2 pos) {
@ -210,11 +210,11 @@ void Container::clear() {
refresh();
}
void Container::listenInterval(float interval, ontimeout callback, int repeat) {
void Container::listenInterval(float interval, OnTimeOut callback, int repeat) {
intervalEvents.push_back({std::move(callback), interval, 0.0f, repeat});
}
void Container::setSize(glm::vec2 size) {
void Container::setSize(const glm::vec2& size) {
if (size == getSize()) {
return;
}

View file

@ -36,9 +36,11 @@ namespace gui {
virtual void remove(const std::string& id);
virtual void scrolled(int value) override;
virtual void setScrollable(bool flag);
void listenInterval(float interval, ontimeout callback, int repeat=-1);
virtual glm::vec2 getContentOffset() override {return glm::vec2(0.0f, scroll);};
virtual void setSize(glm::vec2 size) override;
void listenInterval(float interval, OnTimeOut callback, int repeat=-1);
virtual glm::vec2 getContentOffset() const override {
return glm::vec2(0.0f, scroll);
};
virtual void setSize(const glm::vec2& size) override;
virtual int getScrollStep() const;
virtual void setScrollStep(int step);
virtual void refresh() override;

View file

@ -36,15 +36,14 @@ void InlineFrame::setDocument(const std::shared_ptr<UiDocument>& document) {
}
void InlineFrame::act(float delta) {
Container::act(delta);
if (document || src.empty()) {
return;
if (document == nullptr && !src.empty()) {
const auto& assets = *gui.getEngine().getAssets();
setDocument(assets.getShared<UiDocument>(src));
}
const auto& assets = *gui.getEngine().getAssets();
setDocument(assets.getShared<UiDocument>(src));
Container::act(delta);
}
void InlineFrame::setSize(glm::vec2 size) {
void InlineFrame::setSize(const glm::vec2& size) {
Container::setSize(size);
if (root) {
root->setSize(size);

View file

@ -5,7 +5,7 @@
class UiDocument;
namespace gui {
class InlineFrame : public Container {
class InlineFrame final : public Container {
public:
explicit InlineFrame(GUI& gui);
virtual ~InlineFrame();
@ -14,7 +14,7 @@ namespace gui {
void setDocument(const std::shared_ptr<UiDocument>& document);
void act(float delta) override;
void setSize(glm::vec2 size) override;
void setSize(const glm::vec2& size) override;
const std::string& getSrc() const;
private:

View file

@ -5,7 +5,7 @@
namespace gui {
class Label;
class InputBindBox : public Panel {
class InputBindBox final : public Panel {
protected:
Binding& binding;
glm::vec4 focusedColor {0.1f, 0.15f, 0.35f, 0.75f};
@ -15,13 +15,13 @@ namespace gui {
GUI& gui, Binding& binding, glm::vec4 padding = glm::vec4(6.0f)
);
virtual void drawBackground(
void drawBackground(
const DrawContext& pctx, const Assets& assets
) override;
virtual void clicked(Mousecode button) override;
virtual void keyPressed(Keycode key) override;
virtual bool isFocuskeeper() const override {
void clicked(Mousecode button) override;
void keyPressed(Keycode key) override;
bool isFocuskeeper() const override {
return true;
}
};

View file

@ -528,14 +528,14 @@ void InventoryView::setSelected(int index) {
}
}
void InventoryView::setPos(glm::vec2 pos) {
void InventoryView::setPos(const glm::vec2& pos) {
Container::setPos(pos - origin);
}
void InventoryView::setOrigin(glm::vec2 origin) {
void InventoryView::setOrigin(const glm::vec2& origin) {
this->origin = origin;
}
glm::vec2 InventoryView::getOrigin() const {
const glm::vec2& InventoryView::getOrigin() const {
return origin;
}

View file

@ -109,7 +109,7 @@ namespace gui {
static inline std::string EXCHANGE_SLOT_NAME = "exchange-slot";
};
class InventoryView : public gui::Container {
class InventoryView final : public gui::Container {
const Content* content = nullptr;
std::shared_ptr<Inventory> inventory;
@ -120,10 +120,10 @@ namespace gui {
InventoryView(GUI& gui);
virtual ~InventoryView();
virtual void setPos(glm::vec2 pos) override;
void setPos(const glm::vec2& pos) override;
void setOrigin(glm::vec2 origin);
glm::vec2 getOrigin() const;
void setOrigin(const glm::vec2& origin);
const glm::vec2& getOrigin() const;
void setSelected(int index);

View file

@ -230,14 +230,14 @@ void Label::draw(const DrawContext& pctx, const Assets& assets) {
glm::vec2 pos = calcPos();
switch (align) {
case Align::left: break;
case Align::center: pos.x += (size.x-newsize.x)*0.5f; break;
case Align::right: pos.x += size.x-newsize.x; break;
case Align::LEFT: break;
case Align::CENTER: pos.x += (size.x-newsize.x)*0.5f; break;
case Align::RIGHT: pos.x += size.x-newsize.x; break;
}
switch (valign) {
case Align::top: break;
case Align::center: pos.y += (size.y-newsize.y)*0.5f; break;
case Align::bottom: pos.y += size.y-newsize.y; break;
case Align::TOP: break;
case Align::CENTER: pos.y += (size.y-newsize.y)*0.5f; break;
case Align::BOTTOM: pos.y += size.y-newsize.y; break;
}
textYOffset = pos.y-calcPos().y;
totalLineHeight = lineHeight;

View file

@ -44,7 +44,7 @@ namespace gui {
float lineInterval = 1.5f;
/// @brief Vertical alignment (only when multiline is set to false)
Align valign = Align::center;
Align valign = Align::CENTER;
/// @brief Line separators and wrapping will be ignored if set to false
bool multiline = false;

View file

@ -72,7 +72,7 @@ void Panel::refresh() {
float x = padding.x;
float y = padding.y;
glm::vec2 size = getSize();
if (orientation == Orientation::vertical) {
if (orientation == Orientation::VERTICAL) {
float maxw = size.x;
for (auto& node : nodes) {
const glm::vec4 margin = node->getMargin();

View file

@ -19,7 +19,7 @@ SelectBox::SelectBox(
: Button(gui, selected.text, padding, nullptr, glm::vec2(contentWidth, -1)),
options(std::move(options)) {
listenAction([this](GUI& gui) {
listenClick([this](GUI& gui) {
auto panel = std::make_shared<Panel>(gui, getSize());
panel->setPos(calcPos() + glm::vec2(0, size.y));
for (const auto& option : this->options) {
@ -41,7 +41,7 @@ SelectBox::SelectBox(
});
}
void SelectBox::listenChange(onstringchange&& callback) {
void SelectBox::listenChange(OnStringChange&& callback) {
changeCallbacks.listen(std::move(callback));
}

View file

@ -24,7 +24,7 @@ namespace gui {
const glm::vec4& padding
);
void listenChange(onstringchange&& callback);
void listenChange(OnStringChange&& callback);
void setSelected(const Option& selected);

View file

@ -7,7 +7,7 @@ using namespace gui;
SplitBox::SplitBox(GUI& gui, const glm::vec2& size, float splitPos, Orientation orientation)
: BasePanel(gui, size, glm::vec4(), 4.0f, orientation), splitPos(splitPos) {
setCursor(
orientation == Orientation::vertical ? CursorShape::NS_RESIZE
orientation == Orientation::VERTICAL ? CursorShape::NS_RESIZE
: CursorShape::EW_RESIZE
);
}
@ -17,7 +17,7 @@ void SplitBox::mouseMove(int x, int y) {
auto size = getSize();
glm::ivec2 cursor(x - pos.x, y - pos.y);
int axis = orientation == Orientation::vertical;
int axis = orientation == Orientation::VERTICAL;
int v = cursor[axis];
v = std::max(std::min(static_cast<int>(size[axis]) - 10, v), 10);
@ -61,7 +61,7 @@ void SplitBox::refresh() {
nodeA->setPos(glm::vec2(padding));
const auto& p = padding;
if (orientation == Orientation::vertical) {
if (orientation == Orientation::VERTICAL) {
float splitPos = this->splitPos * size.y;
nodeA->setSize({size.x-p.x-p.z, splitPos - sepRadius - p.y});
nodeB->setSize({size.x-p.x-p.z, size.y - splitPos - sepRadius - p.w});

View file

@ -216,7 +216,7 @@ TextBox::TextBox(GUI& gui, std::wstring placeholder, glm::vec4 padding)
lineNumbersLabel->setSize(
size - glm::vec2(padding.z + padding.x, padding.w + padding.y)
);
lineNumbersLabel->setVerticalAlign(Align::top);
lineNumbersLabel->setVerticalAlign(Align::TOP);
add(lineNumbersLabel);
setHoverColor(glm::vec4(0.05f, 0.1f, 0.2f, 0.75f));
@ -231,7 +231,7 @@ TextBox::~TextBox() = default;
void TextBox::draw(const DrawContext& pctx, const Assets& assets) {
Container::draw(pctx, assets);
if (!isFocused()) {
if (!isFocused() && !keepLineSelection) {
return;
}
const auto& labelText = getText();
@ -252,7 +252,7 @@ void TextBox::draw(const DrawContext& pctx, const Assets& assets) {
float time = gui.getWindow().time();
if (editable && static_cast<int>((time - caretLastMove) * 2) % 2 == 0) {
if (isFocused() && editable && static_cast<int>((time - caretLastMove) * 2) % 2 == 0) {
uint line = label->getLineByTextIndex(caret);
uint lcaret = caret - label->getTextLineOffset(line);
int width = rawTextCache.metrics.calcWidth(input, 0, lcaret);
@ -308,42 +308,44 @@ void TextBox::draw(const DrawContext& pctx, const Assets& assets) {
}
}
if (isFocused() && multiline) {
auto selectionCtx = subctx.sub(batch);
selectionCtx.setBlendMode(BlendMode::addition);
batch->setColor(glm::vec4(1, 1, 1, 0.1f));
uint line = label->getLineByTextIndex(caret);
while (label->isFakeLine(line)) {
line--;
}
do {
int lineY = label->getLineYOffset(line);
batch->setColor(glm::vec4(1, 1, 1, 0.05f));
if (showLineNumbers) {
batch->rect(
lcoord.x - 8,
lcoord.y + lineY,
label->getSize().x,
lineHeight
);
batch->setColor(glm::vec4(1, 1, 1, 0.10f));
batch->rect(
lcoord.x - LINE_NUMBERS_PANE_WIDTH,
lcoord.y + lineY,
LINE_NUMBERS_PANE_WIDTH - 8,
lineHeight
);
} else {
batch->rect(
lcoord.x, lcoord.y + lineY, label->getSize().x, lineHeight
);
}
line++;
} while (line < label->getLinesNumber() && label->isFakeLine(line));
if (!multiline) {
return;
}
auto selectionCtx = subctx.sub(batch);
selectionCtx.setBlendMode(BlendMode::addition);
batch->setColor(glm::vec4(1, 1, 1, 0.1f));
uint line = label->getLineByTextIndex(caret);
while (label->isFakeLine(line)) {
line--;
}
do {
int lineY = label->getLineYOffset(line);
batch->setColor(glm::vec4(1, 1, 1, 0.05f));
if (showLineNumbers) {
batch->rect(
lcoord.x - 8,
lcoord.y + lineY,
label->getSize().x,
lineHeight
);
batch->setColor(glm::vec4(1, 1, 1, 0.10f));
batch->rect(
lcoord.x - LINE_NUMBERS_PANE_WIDTH,
lcoord.y + lineY,
LINE_NUMBERS_PANE_WIDTH - 8,
lineHeight
);
} else {
batch->rect(
lcoord.x, lcoord.y + lineY, label->getSize().x, lineHeight
);
}
line++;
} while (line < label->getLinesNumber() && label->isFakeLine(line));
}
void TextBox::drawBackground(const DrawContext& pctx, const Assets& assets) {
@ -484,7 +486,7 @@ void TextBox::erase(size_t start, size_t length) {
setCaret(caret - length);
}
auto left = input.substr(0, start);
auto right = input.substr(end);
auto right = end >= input.length() ? L"" : input.substr(end);
input = left + right;
}
@ -565,7 +567,7 @@ bool TextBox::isValid() const {
void TextBox::setMultiline(bool multiline) {
this->multiline = multiline;
label->setMultiline(multiline);
label->setVerticalAlign(multiline ? Align::top : Align::center);
label->setVerticalAlign(multiline ? Align::TOP : Align::CENTER);
}
bool TextBox::isMultiline() const {
@ -605,6 +607,14 @@ size_t TextBox::getSelectionEnd() const {
return selectionEnd;
}
void TextBox::setKeepLineSelection(bool flag) {
keepLineSelection = flag;
}
bool TextBox::isKeepLineSelection() const {
return keepLineSelection;
}
void TextBox::setOnEditStart(runnable oneditstart) {
onEditStart = oneditstart;
}
@ -671,6 +681,11 @@ int TextBox::calcIndexAt(int x, int y) const {
);
}
int TextBox::getLineYOffset(int line) const {
if (rawTextCache.fontId == 0) return 0;
return label->getLineYOffset(line);
}
static inline std::wstring get_alphabet(wchar_t c) {
std::wstring alphabet {c};
if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_') {
@ -1034,11 +1049,11 @@ void TextBox::keyPressed(Keycode key) {
resetSelection();
}
}
if (key == Keycode::Z) {
if (editable && key == Keycode::Z) {
historian->undo();
refreshSyntax();
}
if (key == Keycode::Y) {
if (editable && key == Keycode::Y) {
historian->redo();
refreshSyntax();
}

View file

@ -62,6 +62,7 @@ namespace gui {
bool editable = true;
bool autoresize = false;
bool showLineNumbers = false;
bool keepLineSelection = false;
std::string markup;
std::string syntax;
@ -74,7 +75,6 @@ namespace gui {
size_t normalizeIndex(int index);
int calcIndexAt(int x, int y) const;
void setTextOffset(uint x);
bool eraseSelected();
void resetSelection();
@ -186,6 +186,9 @@ namespace gui {
/// @return line position in text
virtual size_t getLinePos(uint line) const;
int calcIndexAt(int x, int y) const;
int getLineYOffset(int line) const;
/// @brief Check text with validator set with setTextValidator
/// @return true if text is valid
virtual bool validate();
@ -220,6 +223,9 @@ namespace gui {
size_t getSelectionStart() const;
size_t getSelectionEnd() const;
void setKeepLineSelection(bool flag);
bool isKeepLineSelection() const;
/// @brief Set runnable called on textbox focus
virtual void setOnEditStart(runnable oneditstart);

View file

@ -64,41 +64,47 @@ UINode* UINode::getParent() const {
return parent;
}
UINode* UINode::listenAction(const onaction& action) {
actions.listen(action);
return this;
void UINode::listenClick(OnAction action) {
actions.listen(UIAction::CLICK, std::move(action));
}
UINode* UINode::listenDoubleClick(const onaction& action) {
doubleClickCallbacks.listen(action);
return this;
void UINode::listenRightClick(OnAction action) {
actions.listen(UIAction::RIGHT_CLICK, std::move(action));
}
UINode* UINode::listenFocus(const onaction& action) {
focusCallbacks.listen(action);
return this;
void UINode::listenDoubleClick(OnAction action) {
actions.listen(UIAction::DOUBLE_CLICK, std::move(action));
}
UINode* UINode::listenDefocus(const onaction& action) {
defocusCallbacks.listen(action);
return this;
void UINode::listenFocus(OnAction action) {
actions.listen(UIAction::FOCUS, std::move(action));
}
void UINode::listenDefocus(OnAction action) {
actions.listen(UIAction::DEFOCUS, std::move(action));
}
void UINode::click(int, int) {
pressed = true;
}
void UINode::clicked(Mousecode button) {
if (button == Mousecode::BUTTON_2) {
actions.notify(UIAction::RIGHT_CLICK, gui);
}
}
void UINode::doubleClick(int x, int y) {
pressed = true;
if (isInside(glm::vec2(x, y))) {
doubleClickCallbacks.notify(gui);
actions.notify(UIAction::DOUBLE_CLICK, gui);
}
}
void UINode::mouseRelease(int x, int y) {
pressed = false;
if (isInside(glm::vec2(x, y))) {
actions.notify(gui);
actions.notify(UIAction::CLICK, gui);
}
}
@ -108,12 +114,12 @@ bool UINode::isPressed() const {
void UINode::onFocus() {
focused = true;
focusCallbacks.notify(gui);
actions.notify(UIAction::FOCUS, gui);
}
void UINode::defocus() {
focused = false;
defocusCallbacks.notify(gui);
actions.notify(UIAction::DEFOCUS, gui);
}
bool UINode::isFocused() const {
@ -197,7 +203,7 @@ glm::vec4 UINode::calcColor() const {
return color;
}
void UINode::setPos(glm::vec2 pos) {
void UINode::setPos(const glm::vec2& pos) {
this->pos = pos;
}
@ -209,7 +215,7 @@ glm::vec2 UINode::getSize() const {
return size;
}
void UINode::setSize(glm::vec2 size) {
void UINode::setSize(const glm::vec2& size) {
this->size = glm::vec2(
glm::max(minSize.x, glm::min(maxSize.x, size.x)),
glm::max(minSize.y, glm::min(maxSize.y, size.y))
@ -220,7 +226,7 @@ glm::vec2 UINode::getMinSize() const {
return minSize;
}
void UINode::setMinSize(glm::vec2 minSize) {
void UINode::setMinSize(const glm::vec2& minSize) {
this->minSize = minSize;
setSize(getSize());
}
@ -229,7 +235,7 @@ glm::vec2 UINode::getMaxSize() const {
return maxSize;
}
void UINode::setMaxSize(glm::vec2 maxSize) {
void UINode::setMaxSize(const glm::vec2& maxSize) {
this->maxSize = maxSize;
setSize(getSize());
}
@ -334,7 +340,7 @@ void UINode::reposition() {
}
void UINode::setGravity(Gravity gravity) {
if (gravity == Gravity::none) {
if (gravity == Gravity::NONE) {
setPositionFunc(nullptr);
return;
}
@ -349,27 +355,27 @@ void UINode::setGravity(Gravity gravity) {
float x = 0.0f, y = 0.0f;
switch (gravity) {
case Gravity::top_left:
case Gravity::center_left:
case Gravity::bottom_left: x = margin.x; break;
case Gravity::top_center:
case Gravity::center_center:
case Gravity::bottom_center: x = (parentSize.x-size.x)/2.0f; break;
case Gravity::top_right:
case Gravity::center_right:
case Gravity::bottom_right: x = parentSize.x-size.x-margin.z; break;
case Gravity::TOP_LEFT:
case Gravity::CENTER_LEFT:
case Gravity::BOTTOM_LEFT: x = margin.x; break;
case Gravity::TOP_CENTER:
case Gravity::CENTER_CENTER:
case Gravity::BOTTOM_CENTER: x = (parentSize.x-size.x)/2.0f; break;
case Gravity::TOP_RIGHT:
case Gravity::CENTER_RIGHT:
case Gravity::BOTTOM_RIGHT: x = parentSize.x-size.x-margin.z; break;
default: break;
}
switch (gravity) {
case Gravity::top_left:
case Gravity::top_center:
case Gravity::top_right: y = margin.y; break;
case Gravity::center_left:
case Gravity::center_center:
case Gravity::center_right: y = (parentSize.y-size.y)/2.0f; break;
case Gravity::bottom_left:
case Gravity::bottom_center:
case Gravity::bottom_right: y = parentSize.y-size.y-margin.w; break;
case Gravity::TOP_LEFT:
case Gravity::TOP_CENTER:
case Gravity::TOP_RIGHT: y = margin.y; break;
case Gravity::CENTER_LEFT:
case Gravity::CENTER_CENTER:
case Gravity::CENTER_RIGHT: y = (parentSize.y-size.y)/2.0f; break;
case Gravity::BOTTOM_LEFT:
case Gravity::BOTTOM_CENTER:
case Gravity::BOTTOM_RIGHT: y = parentSize.y-size.y-margin.w; break;
default: break;
}
return glm::vec2(x, y);

View file

@ -2,6 +2,7 @@
#include "delegates.hpp"
#include "graphics/core/commons.hpp"
#include "util/CallbacksSet.hpp"
#include "window/input.hpp"
#include <glm/glm.hpp>
@ -19,55 +20,64 @@ namespace gui {
class GUI;
class Container;
using onaction = std::function<void(GUI&)>;
using onnumberchange = std::function<void(GUI&, double)>;
using onstringchange = std::function<void(GUI&, const std::string&)>;
using OnAction = std::function<void(GUI&)>;
using OnNumberChange = std::function<void(GUI&, double)>;
using OnStringChange = std::function<void(GUI&, const std::string&)>;
template<typename... Args>
class CallbacksSet {
template<class TagT, typename... Args>
class TaggedCallbacksSet {
public:
using Func = std::function<void(Args...)>;
private:
std::unique_ptr<std::vector<Func>> callbacks;
std::unique_ptr<std::vector<std::pair<TagT, Func>>> callbacks;
public:
void listen(const Func& callback) {
void listen(TagT tag, Func&& callback) {
if (callbacks == nullptr) {
callbacks = std::make_unique<std::vector<Func>>();
callbacks =
std::make_unique<std::vector<std::pair<TagT, Func>>>();
}
callbacks->push_back(callback);
callbacks->push_back({tag, std::move(callback)});
}
void notify(Args&&... args) {
if (callbacks) {
for (auto& callback : *callbacks) {
callback(std::forward<Args>(args)...);
void notify(TagT notifyTag, Args&&... args) {
if (callbacks == nullptr) {
return;
}
for (const auto& [tag, callback] : * callbacks) {
if (tag != notifyTag) {
continue;
}
callback(args...);
}
}
};
using ActionsSet = CallbacksSet<GUI&>;
enum class UIAction {
CLICK, DOUBLE_CLICK, FOCUS, DEFOCUS, RIGHT_CLICK
};
using ActionsSet = TaggedCallbacksSet<UIAction, GUI&>;
using StringCallbacksSet = CallbacksSet<GUI&, const std::string&>;
enum class Align {
left, center, right,
top=left, bottom=right,
LEFT, CENTER, RIGHT,
TOP=LEFT, BOTTOM=RIGHT,
};
enum class Gravity {
none,
NONE,
top_left,
top_center,
top_right,
TOP_LEFT,
TOP_CENTER,
TOP_RIGHT,
center_left,
center_center,
center_right,
CENTER_LEFT,
CENTER_CENTER,
CENTER_RIGHT,
bottom_left,
bottom_center,
bottom_right
BOTTOM_LEFT,
BOTTOM_CENTER,
BOTTOM_RIGHT
};
/// @brief Base abstract class for all UI elements
@ -112,21 +122,15 @@ namespace gui {
/// @brief z-index property specifies the stack order of an element
int zindex = 0;
/// @brief element content alignment (supported by Label only)
Align align = Align::left;
Align align = Align::LEFT;
/// @brief parent element
UINode* parent = nullptr;
/// @brief position supplier for the element (called on parent element size update)
vec2supplier positionfunc = nullptr;
/// @brief size supplier for the element (called on parent element size update)
vec2supplier sizefunc = nullptr;
/// @brief 'onclick' callbacks
/// @brief parameterless callbacks
ActionsSet actions;
/// @brief 'ondoubleclick' callbacks
ActionsSet doubleClickCallbacks;
/// @brief 'onfocus' callbacks
ActionsSet focusCallbacks;
/// @brief 'ondefocus' callbacks
ActionsSet defocusCallbacks;
/// @brief element tooltip text
std::wstring tooltip;
/// @brief element tooltip delay
@ -187,15 +191,16 @@ namespace gui {
/// @brief Get element z-index
int getZIndex() const;
virtual UINode* listenAction(const onaction& action);
virtual UINode* listenDoubleClick(const onaction& action);
virtual UINode* listenFocus(const onaction& action);
virtual UINode* listenDefocus(const onaction& action);
virtual void listenClick(OnAction action);
virtual void listenRightClick(OnAction action);
virtual void listenDoubleClick(OnAction action);
virtual void listenFocus(OnAction action);
virtual void listenDefocus(OnAction action);
virtual void onFocus();
virtual void doubleClick(int x, int y);
virtual void click(int x, int y);
virtual void clicked(Mousecode button) {}
virtual void clicked(Mousecode button);
virtual void mouseMove(int x, int y) {};
virtual void mouseRelease(int x, int y);
virtual void scrolled(int value);
@ -240,17 +245,17 @@ namespace gui {
virtual glm::vec4 calcColor() const;
/// @brief Get inner content offset. Used for scroll
virtual glm::vec2 getContentOffset() {return glm::vec2(0.0f);};
virtual glm::vec2 getContentOffset() const {return glm::vec2(0.0f);};
/// @brief Calculate screen position of the element
virtual glm::vec2 calcPos() const;
virtual void setPos(glm::vec2 pos);
virtual void setPos(const glm::vec2& pos);
virtual glm::vec2 getPos() const;
glm::vec2 getSize() const;
virtual void setSize(glm::vec2 size);
virtual void setSize(const glm::vec2& size);
glm::vec2 getMinSize() const;
virtual void setMinSize(glm::vec2 size);
virtual void setMinSize(const glm::vec2& size);
glm::vec2 getMaxSize() const;
virtual void setMaxSize(glm::vec2 size);
virtual void setMaxSize(const glm::vec2& size);
/// @brief Called in containers when new element added
virtual void refresh() {};
virtual void fullRefresh() {

View file

@ -3,12 +3,12 @@
#include <functional>
namespace gui {
enum class Orientation { vertical, horizontal };
enum class Orientation { VERTICAL, HORIZONTAL };
using ontimeout = std::function<void()>;
using OnTimeOut = std::function<void()>;
struct IntervalEvent {
ontimeout callback;
OnTimeOut callback;
float interval;
float timer;
// -1 - infinity, 1 - one time event

View file

@ -101,7 +101,7 @@ void guiutil::confirm(
gui, glm::vec2(600, 200), glm::vec4(8.0f), 8.0f
);
panel->setGravity(Gravity::center_center);
panel->setGravity(Gravity::CENTER_CENTER);
container->add(panel);
panel->setColor(glm::vec4(0.0f, 0.0f, 0.0f, 0.5f));

View file

@ -30,31 +30,31 @@
using namespace gui;
static Align align_from_string(std::string_view str, Align def) {
if (str == "left") return Align::left;
if (str == "center") return Align::center;
if (str == "right") return Align::right;
if (str == "top") return Align::top;
if (str == "bottom") return Align::bottom;
if (str == "left") return Align::LEFT;
if (str == "center") return Align::CENTER;
if (str == "right") return Align::RIGHT;
if (str == "top") return Align::TOP;
if (str == "bottom") return Align::BOTTOM;
return def;
}
static Gravity gravity_from_string(const std::string& str) {
static const std::unordered_map<std::string, Gravity> gravity_names {
{"top-left", Gravity::top_left},
{"top-center", Gravity::top_center},
{"top-right", Gravity::top_right},
{"center-left", Gravity::center_left},
{"center-center", Gravity::center_center},
{"center-right", Gravity::center_right},
{"bottom-left", Gravity::bottom_left},
{"bottom-center", Gravity::bottom_center},
{"bottom-right", Gravity::bottom_right},
{"top-left", Gravity::TOP_LEFT},
{"top-center", Gravity::TOP_CENTER},
{"top-right", Gravity::TOP_RIGHT},
{"center-left", Gravity::CENTER_LEFT},
{"center-center", Gravity::CENTER_CENTER},
{"center-right", Gravity::CENTER_RIGHT},
{"bottom-left", Gravity::BOTTOM_LEFT},
{"bottom-center", Gravity::BOTTOM_CENTER},
{"bottom-right", Gravity::BOTTOM_RIGHT},
};
auto found = gravity_names.find(str);
if (found != gravity_names.end()) {
return found->second;
}
return Gravity::none;
return Gravity::NONE;
}
static runnable create_runnable(
@ -73,7 +73,7 @@ static runnable create_runnable(
return nullptr;
}
static onaction create_action(
static OnAction create_action(
const UiXmlReader& reader,
const xml::xmlelement& element,
const std::string& name
@ -178,7 +178,11 @@ static void read_uinode(
}
if (auto onclick = create_action(reader, element, "onclick")) {
node.listenAction(onclick);
node.listenClick(onclick);
}
if (auto onclick = create_action(reader, element, "onrightclick")) {
node.listenRightClick(onclick);
}
if (auto onfocus = create_action(reader, element, "onfocus")) {
@ -248,7 +252,7 @@ static void read_base_panel_impl(
if (element.has("orientation")) {
auto& oname = element.attr("orientation").getText();
if (oname == "horizontal") {
panel.setOrientation(Orientation::horizontal);
panel.setOrientation(Orientation::HORIZONTAL);
}
}
}
@ -273,7 +277,7 @@ static void read_panel_impl(
if (element.has("orientation")) {
auto& oname = element.attr("orientation").getText();
if (oname == "horizontal") {
panel.setOrientation(Orientation::horizontal);
panel.setOrientation(Orientation::HORIZONTAL);
}
}
if (subnodes) {
@ -334,7 +338,7 @@ static std::shared_ptr<UINode> read_label(
if (element.has("multiline")) {
label->setMultiline(element.attr("multiline").asBool());
if (!element.has("valign")) {
label->setVerticalAlign(Align::top);
label->setVerticalAlign(Align::TOP);
}
}
if (element.has("text-wrap")) {
@ -360,8 +364,8 @@ static std::shared_ptr<UINode> read_split_box(
float splitPos = element.attr("split-pos", "0.5").asFloat();
Orientation orientation =
element.attr("orientation", "vertical").getText() == "horizontal"
? Orientation::horizontal
: Orientation::vertical;
? Orientation::HORIZONTAL
: Orientation::VERTICAL;
auto splitBox = std::make_shared<SplitBox>(
reader.getGUI(), glm::vec2(), splitPos, orientation
);
@ -569,6 +573,11 @@ static std::shared_ptr<UINode> read_text_box(
if (element.has("line-numbers")) {
textbox->setShowLineNumbers(element.attr("line-numbers").asBool());
}
if (element.has("keep-line-selection")) {
textbox->setKeepLineSelection(
element.attr("keep-line-selection").asBool()
);
}
if (element.has("markup")) {
textbox->setMarkup(element.attr("markup").getText());
}

View file

@ -1,3 +1,5 @@
#pragma once
#include <iostream>
#include <streambuf>
#include <memory>

View file

@ -0,0 +1,220 @@
#include "MemoryDevice.hpp"
#include "../memory_istream.hpp"
#include "../memory_ostream.hpp"
#include "../finalizing_ostream.hpp"
#include <algorithm>
io::MemoryDevice::MemoryDevice() {}
std::filesystem::path io::MemoryDevice::resolve(std::string_view path) {
throw std::runtime_error("unable to resolve filesystem path");
}
std::unique_ptr<std::ostream> io::MemoryDevice::write(std::string_view path) {
std::string filePath = std::string(path);
return std::make_unique<finalizing_ostream>(
std::make_unique<memory_ostream>(),
[this, filePath](auto ostream) {
auto& memoryStream = dynamic_cast<memory_ostream&>(*ostream);
createFile(std::move(filePath), memoryStream.release());
}
);
}
std::unique_ptr<std::istream> io::MemoryDevice::read(std::string_view path) {
const auto& found = nodes.find(std::string(path));
if (found == nodes.end()) {
return nullptr;
}
auto& node = found->second;
if (auto file = node.get_if<File>()) {
if (file->content != nullptr) {
return std::make_unique<memory_view_istream>(file->content);
}
}
return nullptr;
}
size_t io::MemoryDevice::size(std::string_view path) {
const auto& found = nodes.find(std::string(path));
if (found == nodes.end()) {
return 0;
}
return std::visit([](auto&& arg) -> size_t {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, File>) {
return arg.content.size();
} else if constexpr (std::is_same_v<T, Dir>) {
return arg.content.size();
} else {
return 0;
}
}, found->second.data);
}
io::file_time_type io::MemoryDevice::lastWriteTime(std::string_view path) {
return file_time_type::min();
}
bool io::MemoryDevice::exists(std::string_view path) {
if (path.empty()) {
return true;
}
return nodes.find(std::string(path)) != nodes.end();
}
bool io::MemoryDevice::isdir(std::string_view path) {
if (path.empty()) {
return true;
}
const auto& found = nodes.find(std::string(path));
if (found == nodes.end()) {
return false;
}
return found->second.holds_alternative<Dir>();
}
bool io::MemoryDevice::isfile(std::string_view path) {
const auto& found = nodes.find(std::string(path));
if (found == nodes.end()) {
return false;
}
return found->second.holds_alternative<File>();
}
bool io::MemoryDevice::mkdir(std::string_view path) {
return createDir(std::string(path)) != nullptr;
}
bool io::MemoryDevice::mkdirs(std::string_view path) {
io::path dirPath = std::string(path);
std::vector<std::string> parts;
while (!dirPath.pathPart().empty()) {
parts.push_back(dirPath.name());
dirPath = dirPath.parent();
}
for (int i = parts.size() - 1; i >= 0; i--) {
dirPath = dirPath / parts[i];
createDir(dirPath.string());
}
return true;
}
bool io::MemoryDevice::remove(std::string_view path) {
std::string pathString = std::string(path);
const auto& found = nodes.find(pathString);
if (found == nodes.end()) {
return false;
}
if (found->second.holds_alternative<Dir>()) {
const auto& dir = found->second.get_if<Dir>();
if (!dir->content.empty()) {
return false;
}
}
io::path filePath = pathString;
io::path parentPath = filePath.parent();
auto parentDir = getDir(parentPath.string());
if (parentDir) {
auto& content = parentDir->content;
content.erase(
std::remove(content.begin(), content.end(), filePath.name()),
content.end()
);
}
nodes.erase(found);
return true;
}
uint64_t io::MemoryDevice::removeAll(std::string_view path) {
std::string pathString = std::string(path);
const auto& found = nodes.find(pathString);
if (found == nodes.end()) {
return 0;
}
io::path filePath = pathString;
uint64_t count = 0;
if (found->second.holds_alternative<Dir>()) {
auto dir = found->second.get_if<Dir>();
auto files = dir->content;
for (const auto& name : files) {
io::path subPath = filePath / name;
count += removeAll(subPath.string());
}
}
if (remove(pathString)) {
count++;
}
return count;
}
namespace {
struct MemoryPathsGenerator : public io::PathsGenerator {
std::vector<std::string> entries;
size_t index = 0;
MemoryPathsGenerator(std::vector<std::string>&& entries)
: entries(std::move(entries)) {}
bool next(io::path& outPath) override {
if (index >= entries.size()) {
return false;
}
outPath = entries[index++];
return true;
}
};
}
std::unique_ptr<io::PathsGenerator> io::MemoryDevice::list(std::string_view path) {
auto dir = getDir(path);
if (!dir) {
return nullptr;
}
return std::make_unique<MemoryPathsGenerator>(
std::vector<std::string>(dir->content)
);
}
io::MemoryDevice::Dir* io::MemoryDevice::createDir(std::string path) {
io::path filePath = path;
io::path parent = filePath.parent();
auto parentDir = getDir(parent.string());
if (!parentDir) {
return nullptr;
}
parentDir->content.push_back(filePath.name());
auto& node = nodes[std::move(path)];
node.data = Dir {};
return node.get_if<Dir>();
}
io::MemoryDevice::Node* io::MemoryDevice::createFile(
std::string path, util::Buffer<char>&& content
) {
io::path filePath = path;
io::path parent = filePath.parent();
auto dir = getDir(parent.string());
if (!dir) {
return nullptr;
}
dir->content.push_back(filePath.name());
auto& node = nodes[std::move(path)];
node.data = File {std::move(content)};
return &node;
}
io::MemoryDevice::Dir* io::MemoryDevice::getDir(std::string_view path) {
if (path.empty()) {
return &rootDir;
}
const auto& found = nodes.find(std::string(path));
if (found == nodes.end()) {
return nullptr;
}
auto& node = found->second;
return node.get_if<Dir>();
}

View file

@ -0,0 +1,71 @@
#pragma once
#include "Device.hpp"
#include "util/Buffer.hpp"
#include <vector>
#include <variant>
#include <optional>
#include <unordered_map>
namespace io {
/// @brief In-memory filesystem device
class MemoryDevice : public Device {
enum class NodeType {
DIR, FILE
};
struct File {
util::Buffer<char> content;
};
struct Dir {
std::vector<std::string> content;
};
struct Node {
std::variant<Dir, File> data;
NodeType type() const {
return std::visit([](auto&& arg) -> NodeType {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, Dir>) return NodeType::DIR;
else if constexpr (std::is_same_v<T, File>) return NodeType::FILE;
}, data);
}
template <typename T>
bool holds_alternative() const {
return std::holds_alternative<T>(data);
}
template <typename T>
T* get_if() {
return std::get_if<T>(&data);
}
};
public:
MemoryDevice();
std::filesystem::path resolve(std::string_view path) override;
std::unique_ptr<std::ostream> write(std::string_view path) override;
std::unique_ptr<std::istream> read(std::string_view path) override;
size_t size(std::string_view path) override;
file_time_type lastWriteTime(std::string_view path) override;
bool exists(std::string_view path) override;
bool isdir(std::string_view path) override;
bool isfile(std::string_view path) override;
bool mkdir(std::string_view path) override;
bool mkdirs(std::string_view path) override;
bool remove(std::string_view path) override;
uint64_t removeAll(std::string_view path) override;
std::unique_ptr<PathsGenerator> list(std::string_view path) override;
private:
std::unordered_map<std::string, Node> nodes;
Dir rootDir {};
Node* createFile(std::string path, util::Buffer<char>&& content);
Dir* createDir(std::string path);
Dir* getDir(std::string_view path);
};
}

View file

@ -1,3 +1,5 @@
#pragma once
#include "Device.hpp"
namespace io {

View file

@ -239,7 +239,7 @@ std::unique_ptr<std::istream> ZipFileDevice::read(std::string_view path) {
size_t ZipFileDevice::size(std::string_view path) {
const auto& found = entries.find(std::string(path));
if (found == entries.end()) {
return false;
return 0;
}
return found->second.uncompressedSize;
}

View file

@ -0,0 +1,47 @@
#pragma once
#include <memory>
#include <ostream>
#include <functional>
class finalizing_ostream final : public std::ostream {
public:
finalizing_ostream(
std::unique_ptr<std::ostream> inner,
std::function<void(std::unique_ptr<std::ostream>)> on_destruction
)
: std::ostream(inner->rdbuf()),
innerStream(std::move(inner)),
onDestruction(on_destruction) {
}
finalizing_ostream(const finalizing_ostream&) = delete;
finalizing_ostream& operator=(const finalizing_ostream&) = delete;
finalizing_ostream(finalizing_ostream&& other) noexcept
: std::ostream(std::move(other)),
innerStream(std::move(other.innerStream)),
onDestruction(std::move(other.onDestruction)) {
other.onDestruction = nullptr;
}
finalizing_ostream& operator=(finalizing_ostream&& other) noexcept {
if (this != &other) {
std::ostream::operator=(std::move(other));
innerStream = std::move(other.innerStream);
onDestruction = std::move(other.onDestruction);
other.onDestruction = nullptr;
}
return *this;
}
~finalizing_ostream() {
if (onDestruction) {
onDestruction(std::move(innerStream));
}
}
private:
std::unique_ptr<std::ostream> innerStream;
std::function<void(std::unique_ptr<std::ostream>)> onDestruction;
};

View file

@ -32,3 +32,33 @@ public:
private:
memory_streambuf buf;
};
class memory_view_streambuf : public std::streambuf {
public:
explicit memory_view_streambuf(const util::Buffer<char>& buffer)
: buffer(std::move(buffer)) {
char* base = const_cast<char*>(this->buffer.data());
char* end = base + this->buffer.size();
setg(base, base, end);
}
memory_view_streambuf(const memory_view_streambuf&) = delete;
memory_view_streambuf& operator=(const memory_view_streambuf&) = delete;
protected:
int_type underflow() override {
return traits_type::eof();
}
private:
const util::Buffer<char>& buffer;
};
class memory_view_istream : public std::istream {
public:
explicit memory_view_istream(const util::Buffer<char>& buffer)
: std::istream(&buf), buf(buffer) {}
private:
memory_view_streambuf buf;
};

View file

@ -1,3 +1,5 @@
#pragma once
#include <iostream>
#include <streambuf>
#include <cstring>

View file

@ -22,7 +22,7 @@ struct SectionsBuilder {
: map(map), sections(sections) {
}
void section(std::string name) {
void addSection(std::string name) {
sections.push_back(Section {std::move(name), {}});
}
@ -36,15 +36,16 @@ struct SectionsBuilder {
SettingsHandler::SettingsHandler(EngineSettings& settings) {
SectionsBuilder builder(map, sections);
builder.section("audio");
builder.addSection("audio");
builder.add("enabled", &settings.audio.enabled, false);
builder.add("volume-master", &settings.audio.volumeMaster);
builder.add("volume-regular", &settings.audio.volumeRegular);
builder.add("volume-ui", &settings.audio.volumeUI);
builder.add("volume-ambient", &settings.audio.volumeAmbient);
builder.add("volume-music", &settings.audio.volumeMusic);
builder.add("input-device", &settings.audio.inputDevice);
builder.section("display");
builder.addSection("display");
builder.add("width", &settings.display.width);
builder.add("height", &settings.display.height);
builder.add("samples", &settings.display.samples);
@ -53,19 +54,19 @@ SettingsHandler::SettingsHandler(EngineSettings& settings) {
builder.add("window-mode", &settings.display.windowMode);
builder.add("adaptive-menu-fps", &settings.display.adaptiveFpsInMenu);
builder.section("camera");
builder.addSection("camera");
builder.add("sensitivity", &settings.camera.sensitivity);
builder.add("fov", &settings.camera.fov);
builder.add("fov-effects", &settings.camera.fovEffects);
builder.add("shaking", &settings.camera.shaking);
builder.add("inertia", &settings.camera.inertia);
builder.section("chunks");
builder.addSection("chunks");
builder.add("load-distance", &settings.chunks.loadDistance);
builder.add("load-speed", &settings.chunks.loadSpeed);
builder.add("padding", &settings.chunks.padding);
builder.section("graphics");
builder.addSection("graphics");
builder.add("fog-curve", &settings.graphics.fogCurve);
builder.add("backlight", &settings.graphics.backlight);
builder.add("dense-render", &settings.graphics.denseRender);
@ -81,14 +82,14 @@ SettingsHandler::SettingsHandler(EngineSettings& settings) {
builder.add("dense-render-distance", &settings.graphics.denseRenderDistance);
builder.add("soft-lighting", &settings.graphics.softLighting);
builder.section("ui");
builder.addSection("ui");
builder.add("language", &settings.ui.language);
builder.add("world-preview-size", &settings.ui.worldPreviewSize);
builder.section("pathfinding");
builder.addSection("pathfinding");
builder.add("steps-per-async-agent", &settings.pathfinding.stepsPerAsyncAgent);
builder.section("debug");
builder.addSection("debug");
builder.add("generator-test-mode", &settings.debug.generatorTestMode);
builder.add("do-write-lights", &settings.debug.doWriteLights);
builder.add("do-trace-shaders", &settings.debug.doTraceShaders);

View file

@ -15,6 +15,7 @@
#include "voxels/Chunks.hpp"
#include "voxels/GlobalChunks.hpp"
#include "world/Level.hpp"
#include "world/LevelEvents.hpp"
#include "world/World.hpp"
#include "world/generator/WorldGenerator.hpp"
@ -172,13 +173,12 @@ void ChunksController::createChunk(const Player& player, int x, int z) const {
auto chunk = level.chunks->create(x, z);
player.chunks->putChunk(chunk);
auto& chunkFlags = chunk->flags;
if (!chunkFlags.loaded) {
generator->generate(chunk->voxels, x, z);
chunkFlags.unsaved = true;
}
chunk->updateHeights();
level.events->trigger(LevelEventType::CHUNK_PRESENT, chunk.get());
if (!chunkFlags.loadedLights) {
Lighting::prebuildSkyLight(*chunk, *level.content.getIndices());
}

View file

@ -366,7 +366,9 @@ void EngineController::reconfigPacks(
);
}
} else {
auto world = controller->getLevel()->getWorld();
auto level = controller->getLevel();
auto world = level->getWorld();
controller->processBeforeQuit();
controller->saveWorld();
auto names = PacksManager::getNames(world->getPacks());

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