Merge pull request #402 from MihailRis/headless-mode

Headless mode
This commit is contained in:
MihailRis 2025-01-09 03:49:36 +03:00 committed by GitHub
commit 7bbd8bab34
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
248 changed files with 7124 additions and 2902 deletions

View File

@ -1,4 +1,4 @@
name: C/C++ AppImage
name: x86-64 AppImage
on:
push:
@ -20,24 +20,33 @@ jobs:
- uses: actions/checkout@v2
with:
submodules: 'true'
- name: install dependencies
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential libglfw3-dev libglfw3 libglew-dev \
libglm-dev libpng-dev libopenal-dev libluajit-5.1-dev libvorbis-dev libcurl4-openssl-dev cmake squashfs-tools
libglm-dev libpng-dev libopenal-dev libluajit-5.1-dev libvorbis-dev \
libcurl4-openssl-dev libgtest-dev cmake squashfs-tools valgrind
# fix luajit paths
sudo ln -s /usr/lib/x86_64-linux-gnu/libluajit-5.1.a /usr/lib/x86_64-linux-gnu/liblua5.1.a
sudo ln -s /usr/include/luajit-2.1 /usr/include/lua
# install EnTT
git clone https://github.com/skypjack/entt.git
cd entt/build
cmake -DCMAKE_BUILD_TYPE=Release ..
cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo ..
sudo make install
cd ../..
- name: configure
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DVOXELENGINE_BUILD_APPDIR=1
- name: build
- name: Configure
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo -DVOXELENGINE_BUILD_APPDIR=1 -DVOXELENGINE_BUILD_TESTS=ON
- name: Build
run: cmake --build build -t install
- name: Run tests
run: ctest --test-dir build
- name: Run engine tests
timeout-minutes: 1
run: |
chmod +x build/VoxelEngine
chmod +x AppDir/usr/bin/vctest
AppDir/usr/bin/vctest -e build/VoxelEngine -d dev/tests -u build
- name: Build AppImage
uses: AppImageCrafters/build-appimage-action@fe2205a4d6056be47051f7b1b3811106e9814910
env:

View File

@ -39,6 +39,12 @@ jobs:
- name: Run tests
run: ctest --output-on-failure --test-dir build
- name: Run engine tests
timeout-minutes: 1
run: |
chmod +x build/VoxelEngine
chmod +x AppDir/usr/bin/vctest
AppDir/usr/bin/vctest -e build/VoxelEngine -d dev/tests -u build
- name: Create DMG
run: |
mkdir VoxelEngineDmgContent

70
.github/workflows/windows-clang.yml vendored Normal file
View File

@ -0,0 +1,70 @@
name: Windows Build (CLang)
on:
push:
branches: [ "main", "release-**"]
pull_request:
branches: [ "main" ]
jobs:
build-windows:
strategy:
matrix:
include:
- os: windows-latest
compiler: clang
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
with:
submodules: 'true'
- uses: msys2/setup-msys2@v2
id: msys2
name: Setup MSYS2
with:
msystem: clang64
install: >-
mingw-w64-clang-x86_64-toolchain
mingw-w64-clang-x86_64-cmake
mingw-w64-clang-x86_64-make
mingw-w64-clang-x86_64-luajit
git
- name: Set up vcpkg
shell: msys2 {0}
run: |
git clone https://github.com/microsoft/vcpkg.git
cd vcpkg
./bootstrap-vcpkg.bat
./vcpkg integrate install
cd ..
- name: Configure project with CMake and vcpkg
shell: msys2 {0}
run: |
export VCPKG_DEFAULT_TRIPLET=x64-mingw-static
export VCPKG_DEFAULT_HOST_TRIPLET=x64-mingw-static
export VCPKG_ROOT=./vcpkg
mkdir build
cd build
cmake -G "MinGW Makefiles" -DVCPKG_TARGET_TRIPLET=x64-mingw-static -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=./vcpkg/scripts/buildsystems/vcpkg.cmake ..
cmake --build . --config Release
- name: Package for Windows
run: |
mkdir packaged
mkdir packaged/res
cp build/VoxelEngine.exe packaged/
cp build/vctest/vctest.exe packaged/
cp build/*.dll packaged/
cp -r build/res/* packaged/res/
mv packaged/VoxelEngine.exe packaged/VoxelCore.exe
- uses: actions/upload-artifact@v4
with:
name: Windows-Build
path: 'packaged/*'
- name: Run engine tests
shell: msys2 {0}
working-directory: ${{ github.workspace }}
run: |
packaged/vctest.exe -e packaged/VoxelCore.exe -d dev/tests -u build

View File

@ -1,4 +1,4 @@
name: Windows Build
name: MSVC Build
on:
push:
@ -21,29 +21,33 @@ jobs:
with:
submodules: 'true'
- name: Set up vcpkg
- name: Bootstrap vcpkg
shell: pwsh
run: |
git clone https://github.com/microsoft/vcpkg.git
cd vcpkg
.\bootstrap-vcpkg.bat
.\vcpkg integrate install
cd ..
${{ github.workspace }}/vcpkg/bootstrap-vcpkg.bat
- name: Configure and build project with CMake and vcpkg
env:
VCPKG_ROOT: ${{ github.workspace }}/vcpkg
run: |
mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=Release -DVOXELENGINE_BUILD_WINDOWS_VCPKG=ON -DVOXELENGINE_BUILD_TESTS=ON ..
cmake --build . --config Release
cmake --preset default-vs-msvc-windows
cmake --build --preset default-vs-msvc-windows --config Release
- name: Run tests
run: ctest --preset default-vs-msvc-windows
- name: Run engine tests
run: |
build/vctest/Release/vctest.exe -e build/Release/VoxelEngine.exe -d dev/tests -u build
timeout-minutes: 1
- name: Package for Windows
run: |
mkdir packaged
cp -r build/* packaged/
cp C:/Windows/System32/msvcp140.dll packaged/Release/msvcp140.dll
mv packaged/Release/VoxelEngine.exe packaged/Release/VoxelCore.exe
cp -r build/Release/* packaged/
cp build/vctest/Release/vctest.exe packaged/
cp C:/Windows/System32/msvcp140.dll packaged/msvcp140.dll
mv packaged/VoxelEngine.exe packaged/VoxelCore.exe
working-directory: ${{ github.workspace }}
- name: Run tests
run: ctest --output-on-failure --test-dir build
- uses: actions/upload-artifact@v4
with:
name: Windows-Build
path: 'packaged/Release/*'
path: 'packaged/*'

4
.gitignore vendored
View File

@ -36,10 +36,6 @@ Debug/voxel_engine
AppDir
appimage-build/
# for vcpkg
/vcpkg/
.gitmodules
# macOS folder attributes
*.DS_Store

View File

@ -1,18 +1,24 @@
option(VOXELENGINE_BUILD_WINDOWS_VCPKG ON)
if(VOXELENGINE_BUILD_WINDOWS_VCPKG AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/vcpkg/scripts/buildsystems/vcpkg.cmake")
set(CMAKE_TOOLCHAIN_FILE "${CMAKE_CURRENT_SOURCE_DIR}/vcpkg/scripts/buildsystems/vcpkg.cmake" CACHE STRING "")
endif()
cmake_minimum_required(VERSION 3.15)
cmake_minimum_required(VERSION 3.26)
project(VoxelEngine)
option(VOXELENGINE_BUILD_APPDIR OFF)
option(VOXELENGINE_BUILD_TESTS OFF)
option(VOXELENGINE_BUILD_APPDIR "" OFF)
option(VOXELENGINE_BUILD_TESTS "" OFF)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
if (CMAKE_SYSTEM_NAME STREQUAL "Windows")
# We use two types linking: for clang build is static (vcpkg triplet x64-windows-static)
# and for msvc build is dynamic linking (vcpkg triplet x64-windows)
# By default CMAKE_MSVC_RUNTIME_LIBRARY set by MultiThreaded$<$<CONFIG:Debug>:Debug>DLL
if (VCPKG_TARGET_TRIPLET MATCHES "static")
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
endif()
endif()
add_subdirectory(src)
add_executable(${PROJECT_NAME} src/voxel_engine.cpp)
add_executable(${PROJECT_NAME} src/main.cpp)
target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
if(VOXELENGINE_BUILD_APPDIR)
@ -24,7 +30,6 @@ if(MSVC)
set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
endif()
if((CMAKE_BUILD_TYPE EQUAL "Release") OR (CMAKE_BUILD_TYPE EQUAL "RelWithDebInfo"))
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Release>:Release>")
target_compile_options(${PROJECT_NAME} PRIVATE /W4 /MT /O2)
else()
target_compile_options(${PROJECT_NAME} PRIVATE /W4)
@ -39,31 +44,9 @@ else()
if (CMAKE_BUILD_TYPE MATCHES "Debug")
target_compile_options(${PROJECT_NAME} PRIVATE -Og)
endif()
endif()
if(VOXELENGINE_BUILD_WINDOWS_VCPKG AND WIN32)
if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/vcpkg/.git")
find_package(Git QUIET)
if(GIT_FOUND)
message(STATUS "Adding vcpkg as a git submodule...")
execute_process(COMMAND ${GIT_EXECUTABLE} submodule add https://github.com/microsoft/vcpkg.git vcpkg WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
else()
message(FATAL_ERROR "Git not found, cannot add vcpkg submodule.")
endif()
if (WIN32)
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static")
endif()
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/vcpkg/.git")
message(STATUS "Initializing and updating vcpkg submodule...")
execute_process(COMMAND ${GIT_EXECUTABLE} submodule update --init --recursive WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
execute_process(COMMAND ${CMAKE_COMMAND} -E chdir vcpkg ./bootstrap-vcpkg.bat WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
endif()
foreach(CONFIG_TYPE ${CMAKE_CONFIGURATION_TYPES})
string(TOUPPER ${CONFIG_TYPE} CONFIG_TYPE_UPPER)
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_SOURCE_DIR}/res ${CMAKE_BINARY_DIR}/${CONFIG_TYPE_UPPER}/res)
endforeach()
endif()
if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
@ -76,9 +59,18 @@ endif()
target_link_libraries(${PROJECT_NAME} VoxelEngineSrc ${CMAKE_DL_LIBS})
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/res DESTINATION ${CMAKE_CURRENT_BINARY_DIR})
# Deploy res to build dir
add_custom_command(
TARGET ${PROJECT_NAME}
POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory_if_different
${CMAKE_CURRENT_SOURCE_DIR}/res
$<TARGET_FILE_DIR:${PROJECT_NAME}>/res
)
if (VOXELENGINE_BUILD_TESTS)
enable_testing()
add_subdirectory(test)
endif()
endif()
add_subdirectory(vctest)

35
CMakePresets.json Normal file
View File

@ -0,0 +1,35 @@
{
"version": 6,
"configurePresets": [
{
"name": "default-vs-msvc-windows",
"condition": {
"type": "equals",
"rhs": "${hostSystemName}",
"lhs": "Windows"
},
"generator": "Visual Studio 17 2022",
"binaryDir": "${sourceDir}/build",
"cacheVariables": {
"CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake",
"VOXELENGINE_BUILD_TESTS": "ON"
}
}
],
"buildPresets": [
{
"name": "default-vs-msvc-windows",
"configurePreset": "default-vs-msvc-windows",
"configuration": "Debug"
}
],
"testPresets": [
{
"name": "default-vs-msvc-windows",
"configurePreset": "default-vs-msvc-windows",
"output": {
"outputOnFailure": true
}
}
]
}

View File

@ -108,28 +108,32 @@ cmake --build .
>[!NOTE]
> Requirement:
>
> vcpkg, CMake
> vcpkg, CMake, Git
There are two options to use vcpkg:
1. If you have Visual Studio installed, most likely the **VCPKG_ROOT** environment variable will already exist in **Developer Command Prompt for VS**
2. If you want use **vcpkg**, install **vcpkg** from git to you system:
```PowerShell
cd C:/
git clone https://github.com/microsoft/vcpkg.git
cd vcpkg
.\bootstrap-vcpkg.bat
```
After installing **vcpkg**, setup env variable **VCPKG_ROOT** and add it to **PATH**:
```PowerShell
$env:VCPKG_ROOT = "C:\path\to\vcpkg"
$env:PATH = "$env:VCPKG_ROOT;$env:PATH"
```
>[!TIP]
>For troubleshooting you can read full [documentation](https://learn.microsoft.com/ru-ru/vcpkg/get_started/get-started?pivots=shell-powershell) for **vcpkg**
```sh
After installing **vcpkg** you can build project:
```PowerShell
git clone --recursive https://github.com/MihailRis/VoxelEngine-Cpp.git
cd VoxelEngine-Cpp
mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=Release -DVOXELENGINE_BUILD_WINDOWS_VCPKG=ON ..
del CMakeCache.txt
rmdir /s /q CMakeFiles
cmake -DCMAKE_BUILD_TYPE=Release -DVOXELENGINE_BUILD_WINDOWS_VCPKG=ON ..
cmake --build . --config Release
cmake --preset default-vs-msvc-windows
cmake --build --preset default-vs-msvc-windows
```
> [!TIP]
> You can use ```rm CMakeCache.txt``` and ```rm -rf CMakeFiles``` while using Git Bash
> [!WARNING]
> If you have issues during the vcpkg integration, try navigate to ```vcpkg\downloads```
> and extract PowerShell-[version]-win-x86 to ```vcpkg\downloads\tools``` as powershell-core-[version]-windows.
> Then rerun ```cmake -DCMAKE_BUILD_TYPE=Release -DVOXELENGINE_BUILD_WINDOWS_VCPKG=ON ..```
## Build using Docker
### Step 0. Install docker on your system

View File

@ -0,0 +1,24 @@
local util = require("core:tests_util")
-- Create world and prepare settings
util.create_demo_world("core:default")
app.set_setting("chunks.load-distance", 3)
app.set_setting("chunks.load-speed", 1)
-- Create player
local pid = player.create("Xerxes")
player.set_spawnpoint(pid, 0, 100, 0)
player.set_pos(pid, 0, 100, 0)
-- Wait for chunk to load
app.sleep_until(function () return block.get(0, 0, 0) ~= -1 end)
-- Place a falling block
block.place(0, 2, 0, block.index("base:sand"), 0, pid)
app.tick()
-- Check if the block is falling
assert(block.get(0, 2, 0) == 0)
-- Wait for the block to fall
app.sleep_until(function () return block.get(0, 1, 0) == block.index("base:sand") end, 100)

30
dev/tests/chunks.lua Normal file
View File

@ -0,0 +1,30 @@
local util = require "core:tests_util"
util.create_demo_world()
app.set_setting("chunks.load-distance", 3)
app.set_setting("chunks.load-speed", 1)
local pid1 = player.create("Xerxes")
assert(player.get_name(pid1) == "Xerxes")
local pid2 = player.create("Segfault")
assert(player.get_name(pid2) == "Segfault")
local seed = math.floor(math.random() * 1e6)
print("random seed", seed)
math.randomseed(seed)
for i=1,25 do
if i % 5 == 0 then
print(tostring(i*4).." % done")
print("chunks loaded", world.count_chunks())
end
player.set_pos(pid1, math.random() * 100 - 50, 100, math.random() * 100 - 50)
player.set_pos(pid2, math.random() * 200 - 100, 100, math.random() * 200 - 100)
app.tick()
end
player.delete(pid2)
app.close_world(true)
app.delete_world("demo")

50
dev/tests/filesystem.lua Normal file
View File

@ -0,0 +1,50 @@
debug.log("check initial state")
assert(file.exists("config:"))
debug.log("write text file")
assert(file.write("config:text.txt", "example, пример"))
assert(file.exists("config:text.txt"))
debug.log("read text file")
assert(file.read("config:text.txt") == "example, пример")
debug.log("delete file")
file.remove("config:text.txt")
assert(not file.exists("config:text.txt"))
debug.log("create directory")
file.mkdir("config:dir")
assert(file.isdir("config:dir"))
debug.log("remove directory")
file.remove("config:dir")
debug.log("create directories")
file.mkdirs("config:dir/subdir/other")
assert(file.isdir("config:dir/subdir/other"))
debug.log("remove tree")
file.remove_tree("config:dir")
assert(not file.isdir("config:dir"))
debug.log("write binary file")
local bytes = {0xDE, 0xAD, 0xC0, 0xDE}
file.write_bytes("config:binary", bytes)
assert(file.exists("config:binary"))
debug.log("read binary file")
local rbytes = file.read_bytes("config:binary")
assert(#rbytes == #bytes)
for i, b in ipairs(bytes) do
assert(rbytes[i] == b)
end
debug.log("delete file")
file.remove("config:binary")
assert(not file.exists("config:binary"))
debug.log("checking entry points for writeability")
assert(file.is_writeable("config:"))
assert(file.is_writeable("export:"))
assert(not file.is_writeable("user:"))
assert(not file.is_writeable("res:"))

30
dev/tests/world.lua Normal file
View File

@ -0,0 +1,30 @@
-- Create/close/open/close world
-- Open
app.reconfig_packs({"base"}, {})
app.new_world("demo", "2019", "core:default")
assert(world.is_open())
assert(world.get_generator() == "core:default")
app.sleep(1)
assert(world.get_total_time() > 0.0)
print(world.get_total_time())
-- Close
app.close_world(true)
assert(not world.is_open())
-- Reopen
app.open_world("demo")
assert(world.is_open())
assert(world.get_total_time() > 0.0)
assert(world.get_seed() == 2019)
app.tick()
-- Remove base pack
app.reconfig_packs({}, {"base"})
-- World is reopened in post-runnable
app.tick()
-- Close
app.close_world(true)
app.delete_world("demo")

46
dev/valgrind.suppress Normal file
View File

@ -0,0 +1,46 @@
# Valgrind suppression file for VoxelCore
{
<insert_a_suppression_name_here>
Memcheck:Cond
obj:/usr/lib/x86_64-linux-gnu/libluajit-5.1.so.2.1.0
fun:lua_getfield
}
{
<insert_a_suppression_name_here>
Memcheck:Cond
obj:/usr/lib/x86_64-linux-gnu/libluajit-5.1.so.2.1.0
fun:lua_setfield
}
{
<insert_a_suppression_name_here>
Memcheck:Cond
obj:/usr/lib/x86_64-linux-gnu/libluajit-5.1.so.2.1.0
obj:/usr/lib/x86_64-linux-gnu/libluajit-5.1.so.2.1.0
obj:/usr/lib/x86_64-linux-gnu/libluajit-5.1.so.2.1.0
obj:/usr/lib/x86_64-linux-gnu/libluajit-5.1.so.2.1.0
fun:lua_loadx
fun:luaL_loadbufferx
}
{
<insert_a_suppression_name_here>
Memcheck:Cond
obj:/usr/lib/x86_64-linux-gnu/libluajit-5.1.so.2.1.0
fun:lua_pushstring
}
{
<insert_a_suppression_name_here>
Memcheck:Cond
obj:/usr/lib/x86_64-linux-gnu/libluajit-5.1.so.2.1.0
fun:lua_pushlstring
}
{
glewInit internal leak
Memcheck:Leak
match-leak-kinds: definite
fun:malloc
obj:*
obj:*
fun:glXGetClientString
fun:glxewInit
fun:glewInit
}

View File

@ -213,3 +213,39 @@ User properties must be declared in `pack:config/user-props.toml` file:
```
Example: [user properties of pack **base**](../../res/content/base/config/user-props.toml).
## Properties introduced by the `base` pack
### *base:durability*
The time it takes to break a block without tools or effects, measured in seconds.
### Loot - *base:loot*
A list of tables with properties:
```json
{
"item": "pack:item",
"min": 1,
"max": 3,
"chance": 0.5
}
```
- `count` defaults to 1. It does not need to be specified if `min` and `max` are provided.
- `min`, `max` - the minimum and maximum quantity of the item.
- `chance` - the probability of the item dropping. Defaults to 1.0.
It should be noted that the `item` refers specifically to the item. That is, to specify the item of a block, you need to add `.item` after the block name.
Example: `base:dirt.item`.
To generate loot, the function `block_loot(block_id: int)` in the `base:util` module should be used.
## Methods
Methods are used to manage the overwriting of properties when extending a block with other packs.
### `property_name@append`
Adds elements to the end of the list instead of completely overwriting it.

View File

@ -9,9 +9,11 @@ Subsections:
- [UI properties and methods](scripting/ui.md)
- [Entities and components](scripting/ecs.md)
- [Libraries](#)
- [app](scripting/builtins/libapp.md)
- [base64](scripting/builtins/libbase64.md)
- [bjson, json, toml](scripting/filesystem.md)
- [block](scripting/builtins/libblock.md)
- [byteutil](scripting/builtins/libbyteutil.md)
- [cameras](scripting/builtins/libcameras.md)
- [entities](scripting/builtins/libentities.md)
- [file](scripting/builtins/libfile.md)
@ -20,6 +22,7 @@ Subsections:
- [gfx.text3d](3d-text.md#gfxtext3d-library)
- [gui](scripting/builtins/libgui.md)
- [hud](scripting/builtins/libhud.md)
- [input](scripting/builtins/libinput.md)
- [inventory](scripting/builtins/libinventory.md)
- [item](scripting/builtins/libitem.md)
- [mat4](scripting/builtins/libmat4.md)

View File

@ -0,0 +1,147 @@
# *app* library
A library for high-level engine control, available only in script or test mode.
The script/test name without the path and extension is available as `app.script`. The file path can be obtained as:
```lua
local filename = "script:"..app.script..".lua"
```
## Functions
```lua
app.tick()
```
Performs one tick of the main engine loop.
```lua
app.sleep(time: number)
```
Waits for the specified time in seconds, performing the main engine loop.
```lua
app.sleep_until(
-- function that checks the condition for ending the wait
predicate: function() -> bool,
-- the maximum number of engine loop ticks after which
-- a "max ticks exceed" exception will be thrown
[optional] max_ticks = 1e9
)
```
Waits for the condition checked by the function to be true, performing the main engine loop.
```lua
app.quit()
```
Terminates the engine, printing the call stack to trace the function call location.
```lua
app.reconfig_packs(
-- packs to add
add_packs: table,
-- packs to remove
remove_packs: table
)
```
Updates the packs configuration, checking its correctness (dependencies and availability of packs).
Automatically adds dependencies.
To remove all packs from the configuration, you can use `pack.get_installed()`:
```lua
app.reconfig_packs({}, pack.get_installed())
```
In this case, `base` will also be removed from the configuration.
```lua
app.config_packs(
-- expected set of packs (excluding dependencies)
packs: table
)
```
Updates the packs configuration, automatically removing unspecified ones, adding those missing in the previous configuration.
Uses app.reconfig_packs.
```lua
app.new_world(
-- world name
name: str,
-- generation seed
seed: str,
-- generator name
generator: str
)
```
Creates a new world and opens it.
```lua
app.open_world(name: str)
```
Opens a world by name.
```lua
app.reopen_world()
```
Reopens the world.
```lua
app.save_world()
```
Saves the world.
```lua
app.close_world(
-- save the world before closing
[optional] save_world: bool=false
)
```
Closes the world.
```lua
app.delete_world(name: str)
```
Deletes a world by name.
```lua
app.get_version() -> int, int
```
Returns the major and minor versions of the engine.
```lua
app.get_setting(name: str) -> value
```
Returns the value of a setting. Throws an exception if the setting does not exist.
```lua
app.set_setting(name: str, value: value)
```
Sets the value of a setting. Throws an exception if the setting does not exist.
```lua
app.get_setting_info(name: str) -> {
-- default value
def: value,
-- minimum value
[only for numeric settings] min: number,
-- maximum value
[only for numeric settings] max: number
}
```
Returns a table with information about a setting. Throws an exception if the setting does not exist.

View File

@ -67,12 +67,15 @@ Following three functions return direction vectors based on block rotation.
-- Returns X: integer direction vector of the block at specified coordinates.
-- Example: no rotation: 1, 0, 0.
block.get_X(x: int, y: int, z: int) -> int, int, int
block.get_X(id: int, rotation: int) -> int, int, int
-- Same for axis Y. Default: 0, 1, 0.
block.get_Y(x: int, y: int, z: int) -> int, int, int
block.get_Y(id: int, rotation: int) -> int, int, int
-- Same for axis Z. Default: 0, 0, 1.
block.get_Z(x: int, y: int, z: int) -> int, int, int
block.get_Z(id: int, rotation: int) -> int, int, int
-- Returns block rotation index based on used profile.
block.get_rotation(x: int, y: int, z: int) -> int

View File

@ -0,0 +1,70 @@
# *byteutil* library
The library provides functions for working with byte arrays represented as tables or Bytearrays.
```lua
byteutil.pack(format: str, ...) -> Bytearray
byteutil.tpack(format: str, ...) -> table
```
Returns a byte array containing the provided values packed according to the format string. The arguments must exactly match the values required by the format.
The format string consists of special characters and value characters.
Special characters specify the byte order for the subsequent values:
| Character | Byte Order |
| --------- | ------------------- |
| `@` | System |
| `=` | System |
| `<` | Little-endian |
| `>` | Big-endian |
| `!` | Network (big-endian)|
Value characters describe the type and size.
| Character | C++ Equivalent | Lua Type | Size |
| --------- | -------------- | -------- | ------- |
| `b` | int8_t | number | 1 byte |
| `B` | uint8_t | number | 1 byte |
| `?` | bool | boolean | 1 byte |
| `h` | int16_t | number | 2 bytes |
| `H` | uint16_t | number | 2 bytes |
| `i` | int32_t | number | 4 bytes |
| `I` | uint32_t | number | 4 bytes |
| `l` | int64_t | number | 8 bytes |
| `L` | uint64_t | number | 8 bytes |
> [!WARNING]
> Due to the absence of an integer type in Lua for values `l` and `L`, only an output size of 8 bytes is guaranteed; the value may differ from what is expected.
```lua
byteutil.unpack(format: str, bytes: table|Bytearray) -> ...
```
Extracts values from a byte array based on a format string.
Example:
```lua
debug.print(byteutil.tpack('>iBH?', -8, 250, 2019, true))
-- outputs:
-- debug.print(
-- {
-- 255,
-- 255,
-- 255,
-- 248,
-- 250,
-- 7,
-- 227,
-- 1
-- }
-- )
local bytes = byteutil.pack('>iBH?', -8, 250, 2019, true)
print(byteutil.unpack('>iBH?', bytes))
-- outputs:
-- -8 250 2019 true
```

View File

@ -25,6 +25,12 @@ file.read_bytes(path: str) -> array of integers
Read file into bytes array.
```lua
file.is_writeable(path: str) -> bool
```
Checks if the specified path is writable.
```python
file.write(path: str, text: str) -> nil
```
@ -114,3 +120,27 @@ file.read_combined_object(path: str) -> array
```
Combines objects from JSON files of different packs.
```lua
file.name(path: str) --> str
```
Extracts the file name from the path. Example: `world:data/base/config.toml` -> `config.toml`.
``lua
file.stem(path: str) --> str
```
Extracts the file name from the path, removing the extension. Example: `world:data/base/config.toml` -> `config`.
```lua
file.ext(path: str) --> str
```
Extracts the extension from the path. Example: `world:data/base/config.toml` -> `toml`.
```lua
file.prefix(path: str) --> str
```
Extracts the entry point (prefix) from the path. Example: `world:data/base/config.toml` -> `world`.

View File

@ -61,3 +61,34 @@ gui.escape_markup(
```
Escapes markup in text.
```lua
gui.confirm(
-- message (does not translate automatically, use gui.str(...))
message: str,
-- function called upon confirmation
on_confirm: function() -> nil,
-- function called upon denial/cancellation
[optional] on_deny: function() -> nil,
-- text for the confirmation button (default: "Yes")
-- use an empty string for the default value if you want to specify no_text.
[optional] yes_text: str,
-- text for the denial button (default: "No")
[optional] no_text: str,
)
```
Requests confirmation from the user for an action. **Does not** stop code execution.
```lua
gui.load_document(
-- Path to the xml file of the page. Example: `core:layouts/pages/main.xml`
path: str,
-- Name (id) of the document. Example: `core:pages/main`
name: str
-- Table of parameters passed to the on_open event
args: table
) --> str
```
Loads a UI document with its script, returns the name of the document if successfully loaded.

View File

@ -0,0 +1,89 @@
# *input* library
```lua
input.keycode(keyname: str) --> int
```
Returns key code or -1 if unknown
```lua
input.mousecode(mousename: str) --> int
```
Returns mouse button code or -1 if unknown
```lua
input.add_callback(bindname: str, callback: function)
```
Add binding activation callback. Example:
```lua
input.add_callback("hud.inventory", function ()
print("Inventory open key pressed")
end)
```
Callback may be added to a key.
```lua
input.add_callback("key:space", function ()
print("Space pressed")
end)
```
You can also bind the function lifetime to the UI container instead of the HUD.
In that case, `input.add_callback` may be used until the `on_hud_open` is called.
```lua
input.add_callback("key:escape", function ()
print("NO")
return true -- prevents previously assigned functions from being called
end, document.root)
```
```lua
input.get_mouse_pos() --> {int, int}
```
Returns cursor screen position.
```lua
input.get_bindings() --> strings array
```
Returns all binding names.
```lua
input.get_binding_text(bindname: str) --> str
```
Returns text representation of button by binding name.
```lua
input.is_active(bindname: str) --> bool
```
Checks if the binding is active.
```lua
input.set_enabled(bindname: str, flag: bool)
```
Enables/disables binding until leaving the world.
```lua
input.is_pressed(code: str) --> bool
```
Checks input activity using a code consisting of:
- input type: *key* or *mouse*
- input code: [key name](#key names) or mouse button name (left, middle, right)
Example:
```lua
if input.is_pressed("key:enter") then
...
end
```

View File

@ -32,6 +32,21 @@ inventory.size(invid: int) -> int
-- Returns remaining count if could not to add fully.
inventory.add(invid: int, itemid: int, count: int) -> int
-- Returns the index of the first matching slot in the given range.
-- If no matching slot was found, returns nil
inventory.find_by_item(
-- inventory id
invid: int,
-- item id
itemid: int,
-- [optional] index of the slot range start (from 0)
range_begin: int,
-- [optional] index of the slot range end (from 0)
range_end: int,
-- [optional] minimum item count in the slot
min_count: int = 1
) -> int
-- Returns block inventory ID or 0.
inventory.get_block(x: int, y: int, z: int) -> int

View File

@ -16,6 +16,11 @@ end)
-- A variant for binary files, with a byte array instead of a string in the response.
network.get_binary(url: str, callback: function(table|ByteArray))
-- Performs a POST request to the specified URL.
-- Currently, only `Content-Type: application/json` is supported
-- After receiving the response, passes the text to the callback function.
network.post(url: str, data: table, callback: function(str))
```
## TCP Connections
@ -54,6 +59,9 @@ socket:recv(
-- Closes the connection
socket:close()
-- Returns the number of data bytes available for reading
socket:available() --> int
-- Checks that the socket exists and is not closed.
socket:is_alive() --> bool

View File

@ -76,14 +76,15 @@ pack.get_base_packs() -> strings array
Returns the id of all base packages (non-removeable)
```python
```lua
pack.get_info(packid: str) -> {
id: str,
title: str,
creator: str,
description: str,
version: str,
icon: str,
path: str,
icon: str, -- not available in headless mode
dependencies: optional strings array
}
```
@ -95,3 +96,15 @@ Returns information about the pack (not necessarily installed).
- `?` - optional
- `~` - weak
for example `!teal`
To obtain information about multiple packs, use table of ids to avoid re-scanning:one
```lua
pack.get_info(packids: table) -> {id={...}, id2={...}, ...}
```
```lua
pack.assemble(packis: table) -> table
```
Checks the configuration for correctness and adds dependencies, returning the complete configuration.

View File

@ -1,5 +1,17 @@
# *player* library
```lua
player.create(name: str) -> int
```
Creates a player and returns id.
```lua
player.delete(id: int)
```
Deletes a player by id.
```lua
player.get_pos(playerid: int) -> number, number, number
```
@ -70,6 +82,13 @@ player.set_instant_destruction(playerid: int, bool)
Getter and setter for instant destruction of blocks when the `player.destroy` binding is activated.
```lua
player.is_loading_chunks(playerid: int) -> bool
player.set_loading_chunks(playerid: int, bool)
```
Getter and setter of the property that determines whether the player is loading chunks.
``` lua
player.set_spawnpoint(playerid: int, x: number, y: number, z: number)
player.get_spawnpoint(playerid: int) -> number, number, number
@ -84,6 +103,12 @@ player.get_name(playerid: int) -> str
Player name setter and getter
```lua
player.set_selected_slot(playerid: int, slotid: int)
```
Sets the selected slot index
```lua
player.get_selected_block(playerid: int) -> x,y,z
```

View File

@ -36,14 +36,15 @@ world.get_seed() -> int
-- Returns generator name.
world.get_generator() -> str
-- Proves that this is the current time during the day
-- from 0.333(8 am) to 0.833(8 pm).
world.is_day() -> boolean
-- Checks the existence of a world by name.
world.exists(name: str) -> bool
-- Checks that it is the current time at night
-- from 0.833(8 pm) to 0.333(8 am).
-- Checks if the current time is daytime. From 0.333(8am) to 0.833(8pm).
world.is_day() -> bool
-- Checks if the current time is nighttime. From 0.833(8pm) to 0.333(8am).
world.is_night() -> bool
-- Checks the existence of a world by name.
world.exists() -> bool
-- Returns the total number of chunks loaded into memory
world.count_chunks() -> int
```

View File

@ -48,6 +48,7 @@ Properties that apply to all elements:
| tooltip | string | yes | yes | tooltip text |
| tooltipDelay | float | yes | yes | tooltip delay |
| contentOffset | vec2 | yes | *no* | element content offset |
| cursor | string | yes | yes | cursor displayed on hover |
Common element methods:

View File

@ -25,70 +25,4 @@ packid.binding.name="inputtype:codename"
## *input* library
```python
input.keycode(keyname: str) -> int
```
Returns key code or -1 if unknown
```python
input.mousecode(mousename: str) -> int
```
Returns mouse button code or -1 if unknown
```python
input.add_callback(bindname: str, callback: function)
```
Add binding activation callback. Example:
```lua
input.add_callback("hud.inventory", function ()
print("Inventory open key pressed")
end)
```
```python
input.get_mouse_pos() -> {int, int}
```
Returns cursor screen position.
```python
input.get_bindings() -> strings array
```
Returns all binding names.
```python
input.get_binding_text(bindname: str) -> str
```
Returns text representation of button by binding name.
```python
input.is_active(bindname: str) -> bool
```
Checks if the binding is active.
```python
input.set_enabled(bindname: str, flag: bool)
```
Enables/disables binding until leaving the world.
```python
input.is_pressed(code: str) -> bool
```
Checks input activity using a code consisting of:
- input type: *key* or *mouse*
- input code: [key name](#key names) or mouse button name (left, middle, right)
Example:
```lua
if input.is_pressed("key:enter") then
...
end
```
See [*input* library](builtins/libinput.md)

View File

@ -44,6 +44,7 @@ Examples:
- `gravity` - automatic positioning of the element in the container. (Does not work in automatic containers like panel). Values: *top-left, top-center, top-right, center-left, center-center, center-right, bottom-left, bottom-center, bottom-right*.
- `z-index` - determines the order of elements, with a larger value it will overlap elements with a smaller one.
- `interactive` - if false, hovering over the element and all sub-elements will be ignored.
- `cursor` - the cursor displayed when hovering over the element (arrow/text/pointer/crosshair/ew-resize/ns-resize/...).
# Template attributes

View File

@ -224,3 +224,39 @@
```
Пример: [пользовательские свойства пака **base**](../../res/content/base/config/user-props.toml).
## Свойства, вводимые паком `base`
### Прочность - *base:durability*
Время разрушения блока без инструментов и эффектов в секундах.
### Лут - *base:loot*
Список таблиц со свойствами:
```json
{
"item": "пак:предмет",
"min": 1,
"max": 3,
"chance": 0.5
}
```
- count равен 1 по-умолчанию. Не нужно указывать если указаны `min` и `max`.
- min, max - минимальное и максимальное количество предмета.
- chance - вероятность выпадения предмета. По-умолчанию: 1.0.
Следует учитывать, что в item указывается именно предмет. Т.е. чтобы указать предмет блока, нужно добавить `.item` после имени блока.
Пример: `base:dirt.item`.
Для генерации лута следует использовать функцию `block_loot(block_id: int)` в модуле `base:util`.
## Методы
Методы используются для управлением перезаписью свойств при расширении блока другими паками.
### `имя_свойства@append`
Добавляет элементы в конец списка, вместо его полной перезаписи.

View File

@ -9,9 +9,11 @@
- [Свойства и методы UI элементов](scripting/ui.md)
- [Сущности и компоненты](scripting/ecs.md)
- [Библиотеки](#)
- [app](scripting/builtins/libapp.md)
- [base64](scripting/builtins/libbase64.md)
- [bjson, json, toml](scripting/filesystem.md)
- [block](scripting/builtins/libblock.md)
- [byteutil](scripting/builtins/libbyteutil.md)
- [cameras](scripting/builtins/libcameras.md)
- [entities](scripting/builtins/libentities.md)
- [file](scripting/builtins/libfile.md)
@ -20,6 +22,7 @@
- [gfx.text3d](3d-text.md#библиотека-gfxtext3d)
- [gui](scripting/builtins/libgui.md)
- [hud](scripting/builtins/libhud.md)
- [input](scripting/builtins/libinput.md)
- [inventory](scripting/builtins/libinventory.md)
- [item](scripting/builtins/libitem.md)
- [mat4](scripting/builtins/libmat4.md)

View File

@ -0,0 +1,148 @@
# Библиотека *app*
Библиотека для высокоуровневого управления работой движка, доступная только в режиме сценария или теста.
Имя сценария/теста без пути и расширения доступен как `app.script`. Путь к файлу можно получить как:
```lua
local filename = "script:"..app.script..".lua"
```
## Функции
```lua
app.tick()
```
Выполняет один такт основного цикла движка.
```lua
app.sleep(time: number)
```
Ожидает указанное время в секундах, выполняя основной цикл движка.
```lua
app.sleep_until(
-- функция, проверяющее условия завершения ожидания
predicate: function() -> bool,
-- максимальное количество тактов цикла движка, после истечения которых
-- будет брошено исключение "max ticks exceed"
[опционально] max_ticks = 1e9
)
```
Ожидает истинности утверждения (условия), проверяемого функцией, выполнячя основной цикл движка.
```lua
app.quit()
```
Завершает выполнение движка, выводя стек вызовов для ослеживания места вызова функции.
```lua
app.reconfig_packs(
-- добавляемые паки
add_packs: table,
-- удаляемые паки
remove_packs: table
)
```
Обновляет конфигурацию паков, проверяя её корректность (зависимости и доступность паков).
Автоматически добавляет зависимости.
Для удаления всех паков из конфигурации можно использовать `pack.get_installed()`:
```lua
app.reconfig_packs({}, pack.get_installed())
```
В этом случае из конфигурации будет удалён и `base`.
```lua
app.config_packs(
-- ожидаемый набор паков (без учёта зависимостей)
packs: table
)
```
Обновляет конфигурацию паков, автоматически удаляя лишние, добавляя отсутствующие в прошлой конфигурации.
Использует app.reconfig_packs.
```lua
app.new_world(
-- название мира
name: str,
-- зерно генерации
seed: str,
-- название генератора
generator: str
)
```
Создаёт новый мир и открывает его.
```lua
app.open_world(name: str)
```
Открывает мир по названию.
```lua
app.reopen_world()
```
Переоткрывает мир.
```lua
app.save_world()
```
Сохраняет мир.
```lua
app.close_world(
-- сохранить мир перед закрытием
[опционально] save_world: bool=false
)
```
Закрывает мир.
```lua
app.delete_world(name: str)
```
Удаляет мир по названию.
```lua
app.get_version() -> int, int
```
Возвращает мажорную и минорную версии движка.
```lua
app.get_setting(name: str) -> value
```
Возвращает значение настройки. Бросает исключение, если настройки не существует.
```lua
app.set_setting(name: str, value: value)
```
Устанавливает значение настройки. Бросает исключение, если настройки не существует.
```lua
app.get_setting_info(name: str) -> {
-- значение по-умолчанию
def: value
-- минимальное значение
[только числовые настройки] min: number,
-- максимальное значение
[только числовые настройки] max: number
}
```
Возвращает таблицу с информацией о настройке. Бросает исключение, если настройки не существует.

View File

@ -90,12 +90,15 @@ block.raycast(start: vec3, dir: vec3, max_distance: number, [опциональ
-- Возвращает целочисленный единичный вектор X блока на указанных координатах с учётом его вращения (три целых числа).
-- Если поворот отсутствует, возвращает 1, 0, 0
block.get_X(x: int, y: int, z: int) -> int, int, int
block.get_X(id: int, rotation: int) -> int, int, int
-- То же, но для оси Y (по-умолчанию 0, 1, 0)
block.get_Y(x: int, y: int, z: int) -> int, int, int
block.get_Y(id: int, rotation: int) -> int, int, int
-- То же, но для оси Z (по-умолчанию 0, 0, 1)
block.get_Z(x: int, y: int, z: int) -> int, int, int
block.get_Z(id: int, rotation: int) -> int, int, int
-- Возвращает индекс поворота блока в его профиле вращения (не превышает 7).
block.get_rotation(x: int, y: int, z: int) -> int

View File

@ -0,0 +1,71 @@
# Библиотека *byteutil*
Библиотека предоставляет функции для работы с массивами байт, представленными в виде таблиц или Bytearray.
```lua
byteutil.pack(format: str, ...) -> Bytearray
byteutil.tpack(format: str, ...) -> table
```
Возвращает массив байт, содержащий переданные значения, упакованные в соответствии со строкой формата. Аргументы должны точно соответствовать значениям, требуемым форматом.
Строка формата состоит из специальных символов и символов значений.
Специальные символы позволяют указать порядок байт для последующих значений:
| Символ | Порядок байт |
| ------ | -------------------- |
| `@` | Системный |
| `=` | Системный |
| `<` | Little-endian |
| `>` | Big-endian |
| `!` | Сетевой (big-endian) |
Символы значений описывают тип и размер.
| Символ | Аналог в С++ | Тип Lua | Размер |
| ------ | ------------ | -------- | ------- |
| `b` | int8_t | number | 1 байт |
| `B` | uint8_t | number | 1 байт |
| `?` | bool | boolean | 1 байт |
| `h` | int16_t | number | 2 байта |
| `H` | uint16_t | number | 2 байта |
| `i` | int32_t | number | 4 байта |
| `I` | uint32_t | number | 4 байта |
| `l` | int64_t | number | 8 байта |
| `L` | uint64_t | number | 8 байта |
> [!WARNING]
> Из-за отсутствия в Lua целочисленного типа для значений `l` и `L` гарантируется
> только выходной размер в 8 байт, значение может отличаться от ожидаемого.
```lua
byteutil.unpack(format: str, bytes: table|Bytearray) -> ...
```
Извлекает значения из массива байт, ориентируясь на строку формата.
Пример:
```lua
debug.print(byteutil.tpack('>iBH?', -8, 250, 2019, true))
-- выводит:
-- debug.print(
-- {
-- 255,
-- 255,
-- 255,
-- 248,
-- 250,
-- 7,
-- 227,
-- 1
-- }
-- )
local bytes = byteutil.pack('>iBH?', -8, 250, 2019, true)
debug.print(byteutil.unpack('>iBH?', bytes))
-- выводит:
-- -8 250 2019 true
```

View File

@ -25,6 +25,12 @@ file.read_bytes(путь: str) -> array of integers
Читает файл в массив байт.
```lua
file.is_writeable(путь: str) -> bool
```
Проверяет, доступно ли право записи по указанному пути.
```python
file.write(путь: str, текст: str) -> nil
```
@ -114,3 +120,27 @@ file.read_combined_object(путь: str) -> массив
```
Совмещает объекты из JSON файлов разных паков.
```lua
file.name(путь: str) --> str
```
Извлекает имя файла из пути. Пример: `world:data/base/config.toml` -> `config.toml`.
```lua
file.stem(путь: str) --> str
```
Извлекает имя файла из пути, удаляя расширение. Пример: `world:data/base/config.toml` -> `config`.
```lua
file.ext(путь: str) --> str
```
Извлекает расширение из пути. Пример: `world:data/base/config.toml` -> `toml`.
```lua
file.prefix(путь: str) --> str
```
Извлекает точку входа (префикс) из пути. Пример: `world:data/base/config.toml` -> `world`.

View File

@ -58,3 +58,34 @@ gui.escape_markup(
```
Экранирует разметку в тексте.
```lua
gui.confirm(
-- сообщение (не переводится автоматически, используйте gui.str(...))
message: str,
-- функция, вызываемая при подтвержении
on_confirm: function() -> nil,
-- функция, вызываемая при отказе/отмене
[опционально] on_deny: function() -> nil,
-- текст кнопки подтвержения (по-умолчанию: "Да")
-- используйте пустую строку для значения по-умолчанию, если нужно указать no_text.
[опционально] yes_text: str,
-- текст кнопки отказа (по-умолчанию: "Нет")
[опционально] no_text: str,
)
```
Запрашивает у пользователя подтверждение действия. **Не** останавливает выполнение кода.
```lua
gui.load_document(
-- Путь к xml файлу страницы. Пример: `core:layouts/pages/main.xml`
path: str,
-- Имя (id) документа. Пример: `core:pages/main`
name: str
-- Таблица параметров, передаваемых в событие on_open
args: table
) --> str
```
Загружает UI документ с его скриптом, возвращает имя документа, если успешно загружен.

View File

@ -0,0 +1,88 @@
# Библиотека *input*
```lua
input.keycode(keyname: str) --> int
```
Возвращает код клавиши по имени, либо -1
```lua
input.mousecode(mousename: str) --> int
```
Возвращает код кнопки мыши по имени, либо -1
```lua
input.add_callback(bindname: str, callback: function)
```
Назначает функцию, которая будет вызываться при активации привязки. Пример:
```lua
input.add_callback("hud.inventory", function ()
print("Inventory open key pressed")
end)
```
Можно назначить функцию на нажатие клавиши.
```lua
input.add_callback("key:space", function ()
print("Space pressed")
end)
```
Также можно привязать время жизни функции к UI контейнеру, вместо HUD.
В таком случае, `input.add_callback` можно использовать до вызова `on_hud_open`.
```lua
input.add_callback("key:escape", function ()
print("NO")
return true -- предотвращает вызов назначенных ранее функций
end, document.root)
```
```lua
input.get_mouse_pos() --> {int, int}
```
Возвращает позицию курсора на экране.
```lua
input.get_bindings() --> массив строк
```
Возвращает названия всех доступных привязок.
```lua
input.get_binding_text(bindname: str) --> str
```
Возвращает текстовое представление кнопки по имени привязки.
```lua
input.is_active(bindname: str) --> bool
```
Проверяет активность привязки.
```lua
input.set_enabled(bindname: str, flag: bool)
```
Включает/выключает привязку до выхода из мира.
```lua
input.is_pressed(code: str) --> bool
```
Проверяет активность ввода по коду, состоящему из:
- типа ввода: key (клавиша) или mouse (кнопка мыши)
- код ввода: [имя клавиши](#имена-клавиш) или имя кнопки мыши (left, middle, right)
Пример:
```lua
if input.is_pressed("key:enter") then
...
end
```

View File

@ -38,6 +38,21 @@ inventory.add(
count: int
) -> int
-- Возвращает индекс первого подходящего под критерии слота в заданном диапазоне.
-- Если подходящий слот не был найден, возвращает nil
inventory.find_by_item(
-- id инвентаря
invid: int,
-- id предмета
itemid: int,
-- [опционально] индекс начала диапазона слотов (c 0)
range_begin: int,
-- [опционально] индекс конца диапазона слотов (c 0)
range_end: int,
-- [опционально] минимальное количество предмета в слоте
min_count: int = 1
) -> int
-- Функция возвращает id инвентаря блока.
-- Если блок не может иметь инвентарь - возвращает 0.
inventory.get_block(x: int, y: int, z: int) -> int

View File

@ -16,6 +16,11 @@ end)
-- Вариант для двоичных файлов, с массивом байт вместо строки в ответе.
network.get_binary(url: str, callback: function(table|ByteArray))
-- Выполняет POST запрос к указанному URL.
-- На данный момент реализована поддержка только `Content-Type: application/json`
-- После получения ответа, передаёт текст в функцию callback.
network.post(url: str, data: table, callback: function(str))
```
## TCP-Соединения
@ -54,6 +59,9 @@ socket:recv(
-- Закрывает соединение
socket:close()
-- Возвращает количество доступных для чтения байт данных
socket:available() --> int
-- Проверяет, что сокет существует и не закрыт.
socket:is_alive() --> bool

View File

@ -63,14 +63,15 @@ pack.get_base_packs() -> массив строк
Возвращает id всех базовых паков (неудаляемых)
```python
```lua
pack.get_info(packid: str) -> {
id: str,
title: str,
creator: str,
description: str,
version: str,
icon: str,
path: str,
icon: str, -- отсутствует в headless режиме
dependencies: опциональный массив строк
}
```
@ -82,3 +83,16 @@ pack.get_info(packid: str) -> {
- `?` - optional
- `~` - weak
например `!teal`
Для получения информации о нескольких паках используйте таблицу id, чтобы не
производить сканирование для каждого пака:
```lua
pack.get_info(packids: table) -> {id={...}, id2={...}, ...}
```
```lua
pack.assemble(packis: table) -> table
```
Проверяет корректность конфигурации и добавляет зависимости, возвращая полную.

View File

@ -1,5 +1,17 @@
# Библиотека *player*
```lua
player.create(name: str) -> int
```
Создаёт игрока и возвращает его id.
```lua
player.delete(id: int)
```
Удаляет игрока по id.
```lua
player.get_pos(playerid: int) -> number, number, number
```
@ -70,6 +82,13 @@ player.set_instant_destruction(playerid: int, bool)
Геттер и сеттер мнгновенного разрушения блоков при активации привязки `player.destroy`.
```lua
player.is_loading_chunks(playerid: int) -> bool
player.set_loading_chunks(playerid: int, bool)
```
Геттер и сеттер свойства, определяющего, прогружает ли игрок чанки вокруг.
```lua
player.set_spawnpoint(playerid: int, x: number, y: number, z: number)
player.get_spawnpoint(playerid: int) -> number, number, number
@ -84,6 +103,12 @@ player.get_name(playerid: int) -> str
Сеттер и геттер имени игрока
```lua
player.set_selected_slot(playerid: int, slotid: int)
```
Устанавливает индекс выбранного слота
```lua
player.get_selected_block(playerid: int) -> x,y,z
```

View File

@ -36,11 +36,14 @@ world.get_seed() -> int
world.get_generator() -> str
-- Проверяет существование мира по имени.
world.exists() -> bool
world.exists(name: str) -> bool
-- Проверяет является ли текущее время днём. От 0.333(8 утра) до 0.833(8 вечера).
world.is_day() -> bool
-- Проверяет является ли текущее время ночью. От 0.833(8 вечера) до 0.333(8 утра).
world.is_night() -> bool
-- Возвращает общее количество загруженных в память чанков
world.count_chunks() -> int
```

View File

@ -48,6 +48,7 @@ document["worlds-panel"]:clear()
| tooltip | string | да | да | текст всплывающей подсказки |
| tooltipDelay | float | да | да | задержка всплывающей подсказки |
| contentOffset | vec2 | да | *нет* | смещение содержимого |
| cursor | string | да | да | курсор, отображаемый при наведении |
Общие методы элементов:

View File

@ -23,70 +23,4 @@ packid.binding.name="inputtype:codename"
## Библиотека input
```python
input.keycode(keyname: str) -> int
```
Возвращает код клавиши по имени, либо -1
```python
input.mousecode(mousename: str) -> int
```
Возвращает код кнопки мыши по имени, либо -1
```python
input.add_callback(bindname: str, callback: function)
```
Назначает функцию, которая будет вызываться при активации привязки. Пример:
```lua
input.add_callback("hud.inventory", function ()
print("Inventory open key pressed")
end)
```
```python
input.get_mouse_pos() -> {int, int}
```
Возвращает позицию курсора на экране.
```python
input.get_bindings() -> массив строк
```
Возвращает названия всех доступных привязок.
```python
input.get_binding_text(bindname: str) -> str
```
Возвращает текстовое представление кнопки по имени привязки.
```python
input.is_active(bindname: str) -> bool
```
Проверяет активность привязки.
```python
input.set_enabled(bindname: str, flag: bool)
```
Включает/выключает привязку до выхода из мира.
```python
input.is_pressed(code: str) -> bool
```
Проверяет активность ввода по коду, состоящему из:
- типа ввода: key (клавиша) или mouse (кнопка мыши)
- код ввода: [имя клавиши](#имена-клавиш) или имя кнопки мыши (left, middle, right)
Пример:
```lua
if input.is_pressed("key:enter") then
...
end
```
См. [библиотека *input*](builtins/libinput.md)

View File

@ -48,6 +48,7 @@
- `gravity` - автоматическое позиционирование элемента в контейнере. (Не работает в автоматических контейнерах, как panel). Значения: *top-left, top-center, top-right, center-left, center-center, center-right, bottom-left, bottom-center, bottom-right*.
- `z-index` - определяет порядок элементов, при большем значении будет перекрывать элементы с меньшим.
- `interactive` - при значении false наведение на элемент и все под-элементы будет игнорироваться.
- `cursor` - курсор, отображаемый при наведении на элемент (arrow/text/pointer/crosshair/ew-resize/ns-resize/...).
# Атрибуты шаблонов

View File

@ -1,4 +1,5 @@
{
"texture": "bazalt",
"breakable": false
"breakable": false,
"base:durability": 1e9
}

View File

@ -9,5 +9,6 @@
"grounded": true,
"model": "X",
"hitbox": [0.15, 0.0, 0.15, 0.7, 0.7, 0.7],
"base:durability": 0.0
"base:durability": 0.0,
"base:loot": []
}

View File

@ -8,5 +8,8 @@
"grass_side",
"grass_side"
],
"base:durability": 1.3
"base:durability": 1.3,
"base:loot": [
{"item": "base:dirt.item"}
]
}

View File

@ -1 +1,2 @@
"base:durability" = {}
"base:loot" = {}

View File

@ -11,4 +11,34 @@ function util.drop(ppos, itemid, count, pickup_delay)
}})
end
local function calc_loot(loot_table)
local results = {}
for _, loot in ipairs(loot_table) do
local chance = loot.chance or 1
local count = loot.count or 1
local roll = math.random()
if roll < chance then
if loot.min and loot.max then
count = math.random(loot.min, loot.max)
end
if count == 0 then
goto continue
end
table.insert(results, {item=item.index(loot.item), count=count})
end
::continue::
end
return results
end
function util.block_loot(blockid)
local lootscheme = block.properties[blockid]["base:loot"]
if lootscheme then
return calc_loot(lootscheme)
end
return {{item=block.get_picking_item(blockid), count=1}}
end
return util

View File

@ -3,17 +3,32 @@ local body = entity.rigidbody
local rig = entity.skeleton
local blockid = ARGS.block
local blockstates = ARGS.states or 0
if SAVED_DATA.block then
blockid = SAVED_DATA.block
blockstates = SAVED_DATA.states or 0
else
SAVED_DATA.block = blockid
SAVED_DATA.states = blockstates
end
do -- setup visuals
local textures = block.get_textures(block.index(blockid))
local id = block.index(blockid)
local rotation = block.decompose_state(blockstates)[1]
local textures = block.get_textures(id)
for i,t in ipairs(textures) do
rig:set_texture("$"..tostring(i-1), "blocks:"..textures[i])
end
local axisX = {block.get_X(id, rotation)}
local axisY = {block.get_Y(id, rotation)}
local axisZ = {block.get_Z(id, rotation)}
local matrix = {
axisX[1], axisX[2], axisX[3], 0,
axisY[1], axisY[2], axisY[3], 0,
axisZ[1], axisZ[2], axisZ[3], 0,
0, 0, 0, 1
}
rig:set_matrix(0, matrix)
end
function on_grounded()
@ -22,7 +37,7 @@ function on_grounded()
local iy = math.floor(pos[2])
local iz = math.floor(pos[3])
if block.is_replaceable_at(ix, iy, iz) then
block.place(ix, iy, iz, block.index(blockid), 0)
block.place(ix, iy, iz, block.index(blockid), blockstates)
else
local picking_item = block.get_picking_item(block.index(blockid))
local drop = entities.spawn("base:drop", pos, {base__drop={id=picking_item, count=1}})

View File

@ -1,12 +1,16 @@
function on_block_broken(id, x, y, z, playerid)
gfx.particles.emit({x+0.5, y+0.5, z+0.5}, 64, {
lifetime=1.0,
spawn_interval=0.0001,
explosion={4, 4, 4},
texture="blocks:"..block.get_textures(id)[1],
random_sub_uv=0.1,
size={0.1, 0.1, 0.1},
spawn_shape="box",
spawn_spread={0.4, 0.4, 0.4}
})
if gfx then
gfx.particles.emit({x+0.5, y+0.5, z+0.5}, 64, {
lifetime=1.0,
spawn_interval=0.0001,
explosion={4, 4, 4},
texture="blocks:"..block.get_textures(id)[1],
random_sub_uv=0.1,
size={0.1, 0.1, 0.1},
spawn_shape="box",
spawn_spread={0.4, 0.4, 0.4}
})
end
rules.create("do-loot-non-player", true)
end

View File

@ -99,8 +99,14 @@ function refresh()
end
end
local packids = {unpack(packs_installed)}
for i,k in ipairs(packs_available) do
table.insert(packids, k)
end
local packinfos = pack.get_info(packids)
for i,id in ipairs(packs_installed) do
local packinfo = pack.get_info(id)
local packinfo = packinfos[id]
packinfo.index = i
callback = not table.has(base_packs, id) and string.format('move_pack("%s")', id) or nil
packinfo.error = check_dependencies(packinfo)
@ -108,7 +114,7 @@ function refresh()
end
for i,id in ipairs(packs_available) do
local packinfo = pack.get_info(id)
local packinfo = packinfos[id]
packinfo.index = i
callback = string.format('move_pack("%s")', id)
packinfo.error = check_dependencies(packinfo)

View File

@ -247,8 +247,9 @@ function refresh()
local contents = document.contents
contents:clear()
local packinfos = pack.get_info(packs_installed)
for i, id in ipairs(packs_installed) do
local packinfo = pack.get_info(id)
local packinfo = packinfos[id]
packinfo.id = id
packs_installed[i] = {packinfo.id, packinfo.title}

View File

@ -1,5 +1,6 @@
<panel size='400' color='0' interval='1' context='menu'>
<button onclick='menu.page="worlds"'>@Worlds</button>
<button onclick='menu.page="scripts"'>@Scripts</button>
<button onclick='menu.page="settings"'>@Settings</button>
<button onclick='menu.page="content_menu"'>@Contents Menu</button>
<button onclick='core.quit()'>@Quit</button>

View File

@ -0,0 +1,12 @@
<panel size='400' color='#00000030' context='menu'>
<container size='400,32'>
<label pos='2,10'>@Scripts</label>
<image onclick='refresh()' interactive='true' src='gui/refresh'
size='32' gravity='top-right'
color='#FFFFFF80' hover-color='#FFFFFF10'/>
</container>
<panel id="list" size='400' interval='1' color='0' max-length='300'>
<!-- content is generated in script -->
</panel>
<button onclick='menu:back()'>@Back</button>
</panel>

View File

@ -0,0 +1,40 @@
function run_script(path)
debug.log("starting application script "..path)
local code = file.read(path)
local chunk, err = loadstring(code, path)
if chunk == nil then
error(err)
end
setfenv(chunk, setmetatable({app=__vc_app}, {__index=_G}))
start_coroutine(chunk, path)
end
function refresh()
document.list:clear()
local available = pack.get_available()
local infos = pack.get_info(available)
for _, name in ipairs(available) do
local info = infos[name]
local scripts_dir = info.path.."/scripts/app"
if not file.exists(scripts_dir) then
goto continue
end
local files = file.list(scripts_dir)
for _, filename in ipairs(files) do
if file.ext(filename) == "lua" then
document.list:add(gui.template("script", {
pack=name,
name=file.stem(filename),
path=filename
}))
end
end
::continue::
end
end
function on_open()
refresh()
end

View File

@ -0,0 +1,3 @@
<button color='#10305080' hover-color='#10305040' onclick='run_script("%{path}")'>
%{name} [%{pack}]
</button>

View File

@ -13,15 +13,15 @@ local MAX_INT64 = 9223372036854775807
local MIN_INT64 = -9223372036854775808
local function maskHighBytes(num)
return bit.band(num, 0xFF)
return bit.band(num, 0xFF)
end
local function reverse(tbl)
for i=1, math.floor(#tbl / 2) do
local tmp = tbl[i]
tbl[i] = tbl[#tbl - i + 1]
tbl[#tbl - i + 1] = tmp
end
for i=1, math.floor(#tbl / 2) do
local tmp = tbl[i]
tbl[i] = tbl[#tbl - i + 1]
tbl[#tbl - i + 1] = tmp
end
return tbl
end
@ -29,60 +29,60 @@ local orders = { "LE", "BE" }
local fromLEConvertors =
{
LE = function(bytes) return bytes end,
BE = function(bytes) return reverse(bytes) end
LE = function(bytes) return bytes end,
BE = function(bytes) return reverse(bytes) end
}
local toLEConvertors =
{
LE = function(bytes) return bytes end,
BE = function(bytes) return reverse(bytes) end
LE = function(bytes) return bytes end,
BE = function(bytes) return reverse(bytes) end
}
bit_converter.default_order = "LE"
bit_converter.default_order = "BE"
local function fromLE(bytes, orderTo)
if orderTo then
bit_converter.validate_order(orderTo)
return fromLEConvertors[orderTo](bytes)
else return bytes end
if orderTo then
bit_converter.validate_order(orderTo)
return fromLEConvertors[orderTo](bytes)
else return bytes end
end
local function toLE(bytes, orderFrom)
if orderFrom then
bit_converter.validate_order(orderFrom)
return toLEConvertors[orderFrom](bytes)
else return bytes end
if orderFrom then
bit_converter.validate_order(orderFrom)
return toLEConvertors[orderFrom](bytes)
else return bytes end
end
function bit_converter.validate_order(order)
if not bit_converter.is_valid_order(order) then
error("invalid order: "..order)
end
if not bit_converter.is_valid_order(order) then
error("invalid order: "..order)
end
end
function bit_converter.is_valid_order(order) return table.has(orders, order) end
function bit_converter.string_to_bytes(str)
local bytes = { }
local bytes = { }
local len = string.len(str)
local len = string.len(str)
local lenBytes = bit_converter.uint16_to_bytes(len)
local lenBytes = bit_converter.uint16_to_bytes(len)
for i = 1, #lenBytes do
bytes[i] = lenBytes[i]
end
for i = 1, #lenBytes do
bytes[i] = lenBytes[i]
end
for i = 1, len do
bytes[#bytes + 1] = string.byte(string.sub(str, i, i))
end
for i = 1, len do
bytes[#bytes + 1] = string.byte(string.sub(str, i, i))
end
return bytes
return bytes
end
function bit_converter.bool_to_byte(bool)
return bool and 1 or 0
return bool and 1 or 0
end
-- Credits to Iryont <https://github.com/iryont/lua-struct>
@ -151,177 +151,178 @@ end
--
function bit_converter.float32_to_bytes(float, order)
return fromLE(floatOrDoubleToBytes(float, 'f'), order)
return fromLE(floatOrDoubleToBytes(float, 'f'), order)
end
function bit_converter.float64_to_bytes(float, order)
return fromLE(floatOrDoubleToBytes(float, 'd'), order)
return fromLE(floatOrDoubleToBytes(float, 'd'), order)
end
function bit_converter.single_to_bytes(float, order)
on_deprecated_call("bit_converter.float_to_bytes", "bit_converter.float32_to_bytes")
return bit_converter.float32_to_bytes(bytes, order)
on_deprecated_call("bit_converter.float_to_bytes", "bit_converter.float32_to_bytes")
return bit_converter.float32_to_bytes(bytes, order)
end
function bit_converter.double_to_bytes(double, order)
on_deprecated_call("bit_converter.double_to_bytes", "bit_converter.float64_to_bytes")
return bit_converter.float64_to_bytes(bytes, order)
on_deprecated_call("bit_converter.double_to_bytes", "bit_converter.float64_to_bytes")
return bit_converter.float64_to_bytes(bytes, order)
end
local function uint32ToBytes(int, order)
return fromLE({
maskHighBytes(bit.rshift(int, 24)),
maskHighBytes(bit.rshift(int, 16)),
maskHighBytes(bit.rshift(int, 8)),
maskHighBytes(int)
}, order)
return fromLE({
maskHighBytes(int),
maskHighBytes(bit.rshift(int, 8)),
maskHighBytes(bit.rshift(int, 16)),
maskHighBytes(bit.rshift(int, 24))
}, order)
end
local function uint16ToBytes(int, order)
return fromLE({
maskHighBytes(bit.rshift(int, 8)),
maskHighBytes(int)
}, order)
return fromLE({
maskHighBytes(int),
maskHighBytes(bit.rshift(int, 8))
}, order)
end
function bit_converter.uint32_to_bytes(int, order)
if int > MAX_UINT32 or int < MIN_UINT32 then
error("invalid uint32")
end
if int > MAX_UINT32 or int < MIN_UINT32 then
error("invalid uint32")
end
return uint32ToBytes(int, order)
return uint32ToBytes(int, order)
end
function bit_converter.uint16_to_bytes(int, order)
if int > MAX_UINT16 or int < MIN_UINT16 then
error("invalid uint16")
end
if int > MAX_UINT16 or int < MIN_UINT16 then
error("invalid uint16")
end
return uint16ToBytes(int, order)
return uint16ToBytes(int, order)
end
function bit_converter.int64_to_bytes(int, order)
if int > MAX_INT64 or int < MIN_INT64 then
error("invalid int64")
end
if int > MAX_INT64 or int < MIN_INT64 then
error("invalid int64")
end
return fromLE({
maskHighBytes(bit.rshift(int, 56)),
maskHighBytes(bit.rshift(int, 48)),
maskHighBytes(bit.rshift(int, 40)),
maskHighBytes(bit.rshift(int, 32)),
maskHighBytes(bit.rshift(int, 24)),
maskHighBytes(bit.rshift(int, 16)),
maskHighBytes(bit.rshift(int, 8)),
maskHighBytes(int)
}, order)
return fromLE({
maskHighBytes(int),
maskHighBytes(bit.rshift(int, 8)),
maskHighBytes(bit.rshift(int, 16)),
maskHighBytes(bit.rshift(int, 24)),
maskHighBytes(bit.rshift(int, 32)),
maskHighBytes(bit.rshift(int, 40)),
maskHighBytes(bit.rshift(int, 48)),
maskHighBytes(bit.rshift(int, 56))
}, order)
end
function bit_converter.int32_to_bytes(int, order)
on_deprecated_call("bit_converter.int32_to_bytes", "bit_converter.sint32_to_bytes")
on_deprecated_call("bit_converter.int32_to_bytes", "bit_converter.sint32_to_bytes")
if int > MAX_INT32 or int < MIN_INT32 then
error("invalid int32")
end
if int > MAX_INT32 or int < MIN_INT32 then
error("invalid int32")
end
return uint32ToBytes(int + MAX_INT32, order)
return uint32ToBytes(int + MAX_INT32, order)
end
function bit_converter.int16_to_bytes(int, order)
on_deprecated_call("bit_converter.int32_to_bytes", "bit_converter.sint16_to_bytes")
on_deprecated_call("bit_converter.int32_to_bytes", "bit_converter.sint16_to_bytes")
if int > MAX_INT16 or int < MIN_INT16 then
error("invalid int16")
end
if int > MAX_INT16 or int < MIN_INT16 then
error("invalid int16")
end
return uint16ToBytes(int + MAX_INT16, order)
return uint16ToBytes(int + MAX_INT16, order)
end
function bit_converter.sint32_to_bytes(int, order)
if int > MAX_INT32 or int < MIN_INT32 then
error("invalid sint32")
end
if int > MAX_INT32 or int < MIN_INT32 then
error("invalid sint32")
end
return uint32ToBytes(int + MAX_UINT32 + 1, order)
return uint32ToBytes(int + MAX_UINT32 + 1, order)
end
function bit_converter.sint16_to_bytes(int, order)
if int > MAX_INT16 or int < MIN_INT16 then
error("invalid sint16")
end
if int > MAX_INT16 or int < MIN_INT16 then
error("invalid sint16")
end
return uint16ToBytes(int + MAX_UINT16 + 1, order)
return uint16ToBytes(int + MAX_UINT16 + 1, order)
end
function bit_converter.bytes_to_float32(bytes, order)
return bytesToFloatOrDouble(toLE(bytes, order), 'f')
return bytesToFloatOrDouble(toLE(bytes, order), 'f')
end
function bit_converter.bytes_to_float64(bytes, order)
return bytesToFloatOrDouble(toLE(bytes, order), 'd')
return bytesToFloatOrDouble(toLE(bytes, order), 'd')
end
function bit_converter.bytes_to_single(bytes, order)
on_deprecated_call("bit_converter.bytes_to_single", "bit_converter.bytes_to_float32")
return bit_converter.bytes_to_float32(bytes, order)
on_deprecated_call("bit_converter.bytes_to_single", "bit_converter.bytes_to_float32")
return bit_converter.bytes_to_float32(bytes, order)
end
function bit_converter.bytes_to_double(bytes, order)
on_deprecated_call("bit_converter.bytes_to_double", "bit_converter.bytes_to_float64")
return bit_converter.bytes_to_float64(bytes, order)
on_deprecated_call("bit_converter.bytes_to_double", "bit_converter.bytes_to_float64")
return bit_converter.bytes_to_float64(bytes, order)
end
function bit_converter.bytes_to_string(bytes, order)
local len = bit_converter.bytes_to_uint16({ bytes[1], bytes[2] })
local len = bit_converter.bytes_to_uint16({ bytes[1], bytes[2] })
local str = ""
local str = ""
for i = 1, len do
str = str..string.char(bytes[i + 2])
end
for i = 1, len do
str = str..string.char(bytes[i + 2])
end
return str
return str
end
function bit_converter.byte_to_bool(byte)
return byte ~= 0
return byte ~= 0
end
function bit_converter.bytes_to_uint32(bytes, order)
if #bytes < 4 then
error("eof")
end
if #bytes < 4 then
error("eof")
end
bytes = toLE(bytes, order)
bytes = toLE(bytes, order)
return
bit.bor(
bit.bor(
bit.bor(
bit.lshift(bytes[1], 24),
bit.lshift(bytes[2], 16)),
bit.lshift(bytes[3], 8)),bytes[4])
bytes[1],
bit.lshift(bytes[2], 8)),
bit.lshift(bytes[3], 16)),
bit.lshift(bytes[4], 24))
end
function bit_converter.bytes_to_uint16(bytes, order)
if #bytes < 2 then
error("eof")
end
if #bytes < 2 then
error("eof")
end
bytes = toLE(bytes, order)
bytes = toLE(bytes, order)
return
bit.bor(
bit.lshift(bytes[1], 8),
bytes[2], 0)
bit.lshift(bytes[2], 8),
bytes[1], 0)
end
function bit_converter.bytes_to_int64(bytes, order)
if #bytes < 8 then
error("eof")
end
if #bytes < 8 then
error("eof")
end
bytes = toLE(bytes, order)
bytes = toLE(bytes, order)
return
bit.bor(
@ -331,35 +332,35 @@ function bit_converter.bytes_to_int64(bytes, order)
bit.bor(
bit.bor(
bit.bor(
bit.lshift(bytes[1], 56),
bit.lshift(bytes[2], 48)),
bit.lshift(bytes[3], 40)),
bit.lshift(bytes[4], 32)),
bit.lshift(bytes[5], 24)),
bit.lshift(bit.band(bytes[6], 0xFF), 16)),
bit.lshift(bit.band(bytes[7], 0xFF), 8)),bit.band(bytes[8], 0xFF))
bit.lshift(bytes[8], 56),
bit.lshift(bytes[7], 48)),
bit.lshift(bytes[6], 40)),
bit.lshift(bytes[5], 32)),
bit.lshift(bytes[4], 24)),
bit.lshift(bit.band(bytes[3], 0xFF), 16)),
bit.lshift(bit.band(bytes[2], 0xFF), 8)),bit.band(bytes[1], 0xFF))
end
function bit_converter.bytes_to_int32(bytes, order)
on_deprecated_call("bit_converter.bytes_to_int32", "bit_converter.bytes_to_sint32")
return bit_converter.bytes_to_uint32(bytes, order) - MAX_INT32
on_deprecated_call("bit_converter.bytes_to_int32", "bit_converter.bytes_to_sint32")
return bit_converter.bytes_to_uint32(bytes, order) - MAX_INT32
end
function bit_converter.bytes_to_int16(bytes, order)
on_deprecated_call("bit_converter.bytes_to_int16", "bit_converter.bytes_to_sint16")
return bit_converter.bytes_to_uint16(bytes, order) - MAX_INT16
on_deprecated_call("bit_converter.bytes_to_int16", "bit_converter.bytes_to_sint16")
return bit_converter.bytes_to_uint16(bytes, order) - MAX_INT16
end
function bit_converter.bytes_to_sint32(bytes, order)
local num = bit_converter.bytes_to_uint32(bytes, order)
local num = bit_converter.bytes_to_uint32(bytes, order)
return MIN_INT32 * (bit.band(MAX_INT32 + 1, num) ~= 0 and 1 or 0) + bit.band(MAX_INT32, num)
return MIN_INT32 * (bit.band(MAX_INT32 + 1, num) ~= 0 and 1 or 0) + bit.band(MAX_INT32, num)
end
function bit_converter.bytes_to_sint16(bytes, order)
local num = bit_converter.bytes_to_uint16(bytes, order)
local num = bit_converter.bytes_to_uint16(bytes, order)
return MIN_INT16 * (bit.band(MAX_INT16 + 1, num) ~= 0 and 1 or 0) + bit.band(MAX_INT16, num)
return MIN_INT16 * (bit.band(MAX_INT16 + 1, num) ~= 0 and 1 or 0) + bit.band(MAX_INT16, num)
end
return bit_converter

36
res/modules/gui_util.lua Normal file
View File

@ -0,0 +1,36 @@
local gui_util = {}
--- Parse `pagename?arg1=value1&arg2=value2` queries
--- @param query page query string
--- @return page_name, args_table
function gui_util.parse_query(query)
local args = {}
local name
local index = string.find(query, '?')
if index then
local argstr = string.sub(query, index + 1)
name = string.sub(query, 1, index - 1)
for key, value in string.gmatch(argstr, "([^=&]*)=([^&]*)") do
args[key] = value
end
else
name = query
end
return name, args
end
--- @param query page query string
--- @return document_id
function gui_util.load_page(query)
local name, args = gui_util.parse_query(query)
local filename = file.find(string.format("layouts/pages/%s.xml", name))
if filename then
name = file.prefix(filename)..":pages/"..name
gui.load_document(filename, name, args)
return name
end
end
return gui_util

View File

@ -0,0 +1,8 @@
local util = {}
function util.create_demo_world(generator)
app.config_packs({"base"})
app.new_world("demo", "2019", generator or "core:default")
end
return util

View File

@ -31,5 +31,8 @@
"blocks",
"items",
"particles"
],
"models": [
"block"
]
}

View File

@ -40,6 +40,7 @@ local Socket = {__index={
send=function(self, ...) return network.__send(self.id, ...) end,
recv=function(self, ...) return network.__recv(self.id, ...) 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,
is_connected=function(self) return network.__is_connected(self.id) end,
get_address=function(self) return network.__get_address(self.id) end,

58
res/scripts/hud.lua Normal file
View File

@ -0,0 +1,58 @@
function on_hud_open()
input.add_callback("player.pick", function ()
if hud.is_paused() or hud.is_inventory_open() then
return
end
local pid = hud.get_player()
local x, y, z = player.get_selected_block(pid)
if x == nil then
return
end
local id = block.get_picking_item(block.get(x, y, z))
local inv, cur_slot = player.get_inventory(pid)
local slot = inventory.find_by_item(inv, id, 0, 9)
if slot then
player.set_selected_slot(pid, slot)
return
end
if not rules.get("allow-content-access") then
return
end
slot = inventory.find_by_item(inv, 0, 0, 9)
if slot then
cur_slot = slot
end
player.set_selected_slot(pid, cur_slot)
inventory.set(inv, cur_slot, id, 1)
end)
input.add_callback("player.noclip", function ()
if hud.is_paused() or hud.is_inventory_open() then
return
end
local pid = hud.get_player()
if player.is_noclip(pid) then
player.set_flight(pid, false)
player.set_noclip(pid, false)
else
player.set_flight(pid, true)
player.set_noclip(pid, true)
end
end)
input.add_callback("player.flight", function ()
if hud.is_paused() or hud.is_inventory_open() then
return
end
local pid = hud.get_player()
if player.is_noclip(pid) then
return
end
if player.is_flight(pid) then
player.set_flight(pid, false)
else
player.set_flight(pid, true)
player.set_vel(pid, 0, 1, 0)
end
end)
end

View File

@ -7,7 +7,7 @@ local names = {
"hidden", "draw-group", "picking-item", "surface-replacement", "script-name",
"ui-layout", "inventory-size", "tick-interval", "overlay-texture",
"translucent", "fields", "particles", "icon-type", "icon", "placing-block",
"stack-size"
"stack-size", "name"
}
for name, _ in pairs(user_props) do
table.insert(names, name)
@ -40,3 +40,24 @@ make_read_only(block.properties)
for k,v in pairs(block.properties) do
make_read_only(v)
end
local function cache_names(library)
local indices = {}
local names = {}
for id=0,library.defs_count()-1 do
local name = library.properties[id].name
indices[name] = id
names[id] = name
end
function library.name(id)
return names[id]
end
function library.index(name)
return indices[name]
end
end
cache_names(block)
cache_names(item)

View File

@ -9,6 +9,87 @@ function sleep(timesec)
end
end
function tb_frame_tostring(frame)
local s = frame.short_src
if frame.what ~= "C" then
s = s .. ":" .. tostring(frame.currentline)
end
if frame.what == "main" then
s = s .. ": in main chunk"
elseif frame.name then
s = s .. ": in function " .. utf8.escape(frame.name)
end
return s
end
local function complete_app_lib(app)
app.sleep = sleep
app.script = __VC_SCRIPT_NAME
app.new_world = core.new_world
app.open_world = core.open_world
app.save_world = core.save_world
app.close_world = core.close_world
app.reopen_world = core.reopen_world
app.delete_world = core.delete_world
app.reconfig_packs = core.reconfig_packs
app.get_setting = core.get_setting
app.set_setting = core.set_setting
app.tick = coroutine.yield
app.get_version = core.get_version
app.get_setting_info = core.get_setting_info
app.load_content = core.load_content
app.reset_content = core.reset_content
function app.config_packs(packs_list)
-- Check if packs are valid and add dependencies to the configuration
packs_list = pack.assemble(packs_list)
local installed = pack.get_installed()
local toremove = {}
for _, packid in ipairs(installed) do
if not table.has(packs_list, packid) then
table.insert(toremove, packid)
end
end
local toadd = {}
for _, packid in ipairs(packs_list) do
if not table.has(installed, packid) then
table.insert(toadd, packid)
end
end
app.reconfig_packs(toadd, toremove)
end
function app.quit()
local tb = debug.get_traceback(1)
local s = "app.quit() traceback:"
for i, frame in ipairs(tb) do
s = s .. "\n\t"..tb_frame_tostring(frame)
end
debug.log(s)
core.quit()
coroutine.yield()
end
function app.sleep_until(predicate, max_ticks)
max_ticks = max_ticks or 1e9
local ticks = 0
while ticks < max_ticks and not predicate() do
app.tick()
ticks = ticks + 1
end
if ticks == max_ticks then
error("max ticks exceed")
end
end
end
if app then
complete_app_lib(app)
elseif __vc_app then
complete_app_lib(__vc_app)
end
------------------------------------------------
------------------- Events ---------------------
------------------------------------------------
@ -54,7 +135,12 @@ function events.emit(event, ...)
return nil
end
for _, func in ipairs(handlers) do
result = result or func(...)
local status, newres = xpcall(func, __vc__error, ...)
if not status then
debug.error("error in event ("..event..") handler: "..newres)
else
result = result or newres
end
end
return result
end
@ -85,7 +171,7 @@ function Document.new(docname)
end
local _RadioGroup = {}
function _RadioGroup.set(self, key)
function _RadioGroup:set(key)
if type(self) ~= 'table' then
error("called as non-OOP via '.', use radiogroup:set")
end
@ -98,7 +184,7 @@ function _RadioGroup.set(self, key)
self.callback(key)
end
end
function _RadioGroup.__call(self, elements, onset, default)
function _RadioGroup:__call(elements, onset, default)
local group = setmetatable({
elements=elements,
callback=onset,
@ -114,20 +200,8 @@ _GUI_ROOT = Document.new("core:root")
_MENU = _GUI_ROOT.menu
menu = _MENU
local __post_runnables = {}
function __process_post_runnables()
if #__post_runnables then
for _, func in ipairs(__post_runnables) do
func()
end
__post_runnables = {}
end
end
function time.post_runnable(runnable)
table.insert(__post_runnables, runnable)
end
local gui_util = require "core:gui_util"
__vc_page_loader = gui_util.load_page
--- Console library extension ---
console.cheats = {}
@ -272,7 +346,6 @@ function __vc_on_hud_open()
_rules.create("allow-content-access", hud._is_content_access(), function(value)
hud._set_content_access(value)
input.set_enabled("player.pick", value)
end)
_rules.create("allow-flight", true, function(value)
input.set_enabled("player.flight", value)
@ -328,6 +401,86 @@ function __vc_on_world_quit()
_rules.clear()
end
local __vc_coroutines = {}
local __vc_named_coroutines = {}
local __vc_next_coroutine = 1
local __vc_coroutine_error = nil
function __vc_start_coroutine(chunk)
local co = coroutine.create(function()
local status, err = pcall(chunk)
if not status then
__vc_coroutine_error = err
end
end)
local id = __vc_next_coroutine
__vc_next_coroutine = __vc_next_coroutine + 1
__vc_coroutines[id] = co
return id
end
function __vc_resume_coroutine(id)
local co = __vc_coroutines[id]
if co then
coroutine.resume(co)
if __vc_coroutine_error then
debug.error(__vc_coroutine_error)
error(__vc_coroutine_error)
end
return coroutine.status(co) ~= "dead"
end
return false
end
function __vc_stop_coroutine(id)
local co = __vc_coroutines[id]
if co then
if coroutine.close then
coroutine.close(co)
end
__vc_coroutines[id] = nil
end
end
function start_coroutine(chunk, name)
local co = coroutine.create(function()
local status, error = xpcall(chunk, __vc__error)
if not status then
debug.error(error)
end
end)
__vc_named_coroutines[name] = co
end
local __post_runnables = {}
function __process_post_runnables()
if #__post_runnables then
for _, func in ipairs(__post_runnables) do
local status, result = xpcall(func, __vc__error)
if not status then
debug.error("error in post_runnable: "..result)
end
end
__post_runnables = {}
end
local dead = {}
for name, co in pairs(__vc_named_coroutines) do
coroutine.resume(co)
if coroutine.status(co) == "dead" then
table.insert(dead, name)
end
end
for _, name in ipairs(dead) do
__vc_named_coroutines[name] = nil
end
end
function time.post_runnable(runnable)
table.insert(__post_runnables, runnable)
end
assets = {}
assets.load_texture = core.__load_texture

View File

@ -34,11 +34,11 @@ end
function timeit(iters, func, ...)
local tm = time.uptime()
local tm = os.clock()
for i=1,iters do
func(...)
end
print("[time mcs]", (time.uptime()-tm) * 1000000)
print("[time mcs]", (os.clock()-tm) * 1000000)
end
----------------------------------------------
@ -361,3 +361,20 @@ function __vc_warning(msg, detail, n)
"core:warning", msg, detail, debug.get_traceback(1 + (n or 0)))
end
end
function file.name(path)
return path:match("([^:/\\]+)$")
end
function file.stem(path)
local name = file.name(path)
return name:match("(.+)%.[^%.]+$") or name
end
function file.ext(path)
return path:match("%.([^:/\\]+)$")
end
function file.prefix(path)
return path:match("^([^:]+)")
end

View File

@ -34,6 +34,7 @@ graphics.dense-render.tooltip=Включает прозрачность блок
menu.Apply=Применить
menu.Audio=Звук
menu.Back to Main Menu=Вернуться в Меню
menu.Scripts=Сценарии
menu.Content Error=Ошибка Контента
menu.Content=Контент
menu.Continue=Продолжить

View File

@ -4,15 +4,19 @@ set(CMAKE_CXX_STANDARD 17)
file(GLOB_RECURSE HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/*.hpp)
file(GLOB_RECURSE SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp)
list(REMOVE_ITEM SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/voxel_engine.cpp)
list(REMOVE_ITEM SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp)
add_library(${PROJECT_NAME} ${SOURCES} ${HEADERS})
option(VOXELENGINE_BUILD_WINDOWS_VCPKG ON)
add_library(${PROJECT_NAME} STATIC ${SOURCES} ${HEADERS})
find_package(OpenGL REQUIRED)
find_package(GLEW REQUIRED)
find_package(OpenAL REQUIRED)
if (CMAKE_SYSTEM_NAME STREQUAL "Windows")
# specific for vcpkg
find_package(OpenAL CONFIG REQUIRED)
set(OPENAL_LIBRARY OpenAL::OpenAL)
else()
find_package(OpenAL REQUIRED)
endif()
find_package(ZLIB REQUIRED)
find_package(PNG REQUIRED)
find_package(CURL REQUIRED)
@ -20,19 +24,23 @@ if (NOT APPLE)
find_package(EnTT REQUIRED)
endif()
if (WIN32)
if(VOXELENGINE_BUILD_WINDOWS_VCPKG)
set(LUA_LIBRARIES "${CMAKE_CURRENT_SOURCE_DIR}/../vcpkg/packages/luajit_x64-windows/lib/lua51.lib")
set(LUA_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../vcpkg/packages/luajit_x64-windows/include/luajit")
find_package(glfw3 REQUIRED)
find_package(glm REQUIRED)
find_package(vorbis REQUIRED)
set(VORBISLIB Vorbis::vorbis Vorbis::vorbisfile)
set(LIBS "")
if (CMAKE_SYSTEM_NAME STREQUAL "Windows")
# Use directly linking to lib instead PkgConfig (because pkg-config dont install on windows as default)
# TODO: Do it with findLua.
if (MSVC)
set(LUA_INCLUDE_DIR "$ENV{VCPKG_ROOT}/packages/luajit_${VCPKG_TARGET_TRIPLET}/include/luajit")
find_package(Lua REQUIRED)
else()
find_package(Lua REQUIRED)
set(VORBISLIB vorbis vorbisfile) # not tested
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/libs/glfw)
# Used for mingw-clang cross compiling from msys2
set(LIBS ${LIBS} luajit-5.1)
endif()
find_package(glfw3 REQUIRED)
find_package(glm REQUIRED)
find_package(vorbis REQUIRED)
set(VORBISLIB Vorbis::vorbis Vorbis::vorbisfile)
elseif(APPLE)
find_package(PkgConfig)
pkg_check_modules(LUAJIT REQUIRED luajit)
@ -53,8 +61,6 @@ else()
set(VORBISLIB ${VORBIS_LDFLAGS})
endif()
set(LIBS "")
if(UNIX)
find_package(glfw3 3.3 REQUIRED)
find_package(Threads REQUIRED)

View File

@ -8,6 +8,9 @@
#include "coders/wav.hpp"
#include "AL/ALAudio.hpp"
#include "NoAudio.hpp"
#include "debug/Logger.hpp"
static debug::Logger logger("audio");
namespace audio {
static speakerid_t nextId = 1;
@ -147,10 +150,14 @@ public:
void audio::initialize(bool enabled) {
if (enabled) {
logger.info() << "initializing ALAudio backend";
backend = ALAudio::create().release();
}
if (backend == nullptr) {
std::cerr << "could not to initialize audio" << std::endl;
if (enabled) {
std::cerr << "could not to initialize audio" << std::endl;
}
logger.info() << "initializing NoAudio backend";
backend = NoAudio::create().release();
}
create_channel("master");

View File

@ -6,6 +6,10 @@
#include "util/data_io.hpp"
ByteBuilder::ByteBuilder(size_t size) {
buffer.reserve(size);
}
void ByteBuilder::put(ubyte b) {
buffer.push_back(b);
}
@ -31,37 +35,37 @@ void ByteBuilder::put(const ubyte* arr, size_t size) {
}
}
void ByteBuilder::putInt16(int16_t val) {
void ByteBuilder::putInt16(int16_t val, bool bigEndian) {
size_t size = buffer.size();
buffer.resize(buffer.size() + sizeof(int16_t));
val = dataio::h2le(val);
val = bigEndian ? dataio::h2be(val) : dataio::h2le(val);
std::memcpy(buffer.data()+size, &val, sizeof(int16_t));
}
void ByteBuilder::putInt32(int32_t val) {
void ByteBuilder::putInt32(int32_t val, bool bigEndian) {
size_t size = buffer.size();
buffer.resize(buffer.size() + sizeof(int32_t));
val = dataio::h2le(val);
val = bigEndian ? dataio::h2be(val) : dataio::h2le(val);
std::memcpy(buffer.data()+size, &val, sizeof(int32_t));
}
void ByteBuilder::putInt64(int64_t val) {
void ByteBuilder::putInt64(int64_t val, bool bigEndian) {
size_t size = buffer.size();
buffer.resize(buffer.size() + sizeof(int64_t));
val = dataio::h2le(val);
val = bigEndian ? dataio::h2be(val) : dataio::h2le(val);
std::memcpy(buffer.data()+size, &val, sizeof(int64_t));
}
void ByteBuilder::putFloat32(float val) {
void ByteBuilder::putFloat32(float val, bool bigEndian) {
int32_t i32_val;
std::memcpy(&i32_val, &val, sizeof(int32_t));
putInt32(i32_val);
putInt32(i32_val, bigEndian);
}
void ByteBuilder::putFloat64(double val) {
void ByteBuilder::putFloat64(double val, bool bigEndian) {
int64_t i64_val;
std::memcpy(&i64_val, &val, sizeof(int64_t));
putInt64(i64_val);
putInt64(i64_val, bigEndian);
}
void ByteBuilder::set(size_t position, ubyte val) {
@ -95,6 +99,10 @@ ByteReader::ByteReader(const ubyte* data) : data(data), size(4), pos(0) {
size = getInt32();
}
ByteReader::ByteReader(const std::vector<ubyte>& data)
: data(data.data()), size(data.size()), pos(0) {
}
void ByteReader::checkMagic(const char* data, size_t size) {
if (pos + size >= this->size) {
throw std::runtime_error("invalid magic number");
@ -129,45 +137,51 @@ ubyte ByteReader::peek() {
return data[pos];
}
int16_t ByteReader::getInt16() {
int16_t ByteReader::getInt16(bool bigEndian) {
if (pos + sizeof(int16_t) > size) {
throw std::runtime_error("buffer underflow");
}
int16_t value;
std::memcpy(&value, data + pos, sizeof(int16_t));
pos += sizeof(int16_t);
return dataio::le2h(value);
return bigEndian ? dataio::be2h(value) : dataio::le2h(value);
}
int32_t ByteReader::getInt32() {
int32_t ByteReader::getInt32(bool bigEndian) {
if (pos + sizeof(int32_t) > size) {
throw std::runtime_error("buffer underflow");
}
int32_t value;
std::memcpy(&value, data + pos, sizeof(int32_t));
pos += sizeof(int32_t);
return dataio::le2h(value);
return bigEndian ? dataio::be2h(value) : dataio::le2h(value);
}
int64_t ByteReader::getInt64() {
int64_t ByteReader::getInt64(bool bigEndian) {
if (pos + sizeof(int64_t) > size) {
throw std::runtime_error("buffer underflow");
}
int64_t value;
std::memcpy(&value, data + pos, sizeof(int64_t));
pos += sizeof(int64_t);
return dataio::le2h(value);
return bigEndian ? dataio::be2h(value) : dataio::le2h(value);
}
float ByteReader::getFloat32() {
float ByteReader::getFloat32(bool bigEndian) {
int32_t i32_val = getInt32();
if (bigEndian) {
i32_val = dataio::be2h(i32_val);
}
float val;
std::memcpy(&val, &i32_val, sizeof(float));
return val;
}
double ByteReader::getFloat64() {
double ByteReader::getFloat64(bool bigEndian) {
int64_t i64_val = getInt64();
if (bigEndian) {
i64_val = dataio::be2h(i64_val);
}
double val;
std::memcpy(&val, &i64_val, sizeof(double));
return val;

View File

@ -8,20 +8,23 @@
class ByteBuilder {
std::vector<ubyte> buffer;
public:
ByteBuilder() = default;
ByteBuilder(size_t size);
/// @brief Write one byte (8 bit unsigned integer)
void put(ubyte b);
/// @brief Write c-string (bytes array terminated with '\00')
void putCStr(const char* str);
/// @brief Write signed 16 bit little-endian integer
void putInt16(int16_t val);
void putInt16(int16_t val, bool bigEndian = false);
/// @brief Write signed 32 bit integer
void putInt32(int32_t val);
void putInt32(int32_t val, bool bigEndian = false);
/// @brief Write signed 64 bit integer
void putInt64(int64_t val);
void putInt64(int64_t val, bool bigEndian = false);
/// @brief Write 32 bit floating-point number
void putFloat32(float val);
void putFloat32(float val, bool bigEndian = false);
/// @brief Write 64 bit floating-point number
void putFloat64(double val);
void putFloat64(double val, bool bigEndian = false);
/// @brief Write string (uint32 length + bytes)
void put(const std::string& s);
@ -50,6 +53,7 @@ class ByteReader {
public:
ByteReader(const ubyte* data, size_t size);
ByteReader(const ubyte* data);
ByteReader(const std::vector<ubyte>& data);
void checkMagic(const char* data, size_t size);
/// @brief Get N bytes
@ -59,15 +63,15 @@ public:
/// @brief Read one byte (unsigned 8 bit integer) without pointer move
ubyte peek();
/// @brief Read signed 16 bit little-endian integer
int16_t getInt16();
int16_t getInt16(bool bigEndian = false);
/// @brief Read signed 32 bit little-endian integer
int32_t getInt32();
int32_t getInt32(bool bigEndian = false);
/// @brief Read signed 64 bit little-endian integer
int64_t getInt64();
int64_t getInt64(bool bigEndian = false);
/// @brief Read 32 bit floating-point number
float getFloat32();
float getFloat32(bool bigEndian = false);
/// @brief Read 64 bit floating-point number
double getFloat64();
double getFloat64(bool bigEndian = false);
/// @brief Read C-String
const char* getCString();
/// @brief Read string with unsigned 32 bit number before (length)

View File

@ -92,11 +92,11 @@ glm::vec4 Attribute::asColor() const {
throw std::runtime_error("#RRGGBB or #RRGGBBAA required");
}
int a = 255;
int r = (hexchar2int(text[1]) << 4) | hexchar2int(text[2]);
int g = (hexchar2int(text[3]) << 4) | hexchar2int(text[4]);
int b = (hexchar2int(text[5]) << 4) | hexchar2int(text[6]);
int r = (std::max(0, hexchar2int(text[1])) << 4) | hexchar2int(text[2]);
int g = (std::max(0, hexchar2int(text[3])) << 4) | hexchar2int(text[4]);
int b = (std::max(0, hexchar2int(text[5])) << 4) | hexchar2int(text[6]);
if (text.length() == 9) {
a = (hexchar2int(text[7]) << 4) | hexchar2int(text[8]);
a = (std::max(0, hexchar2int(text[7])) << 4) | hexchar2int(text[8]);
}
return glm::vec4(r / 255.f, g / 255.f, b / 255.f, a / 255.f);
} else {

View File

@ -35,6 +35,11 @@ inline constexpr int CHUNK_D = 16;
inline constexpr uint VOXEL_USER_BITS = 8;
inline constexpr uint VOXEL_USER_BITS_OFFSET = sizeof(blockstate_t)*8-VOXEL_USER_BITS;
/// @brief % unordered map max average buckets load factor.
/// Low value gives significant performance impact by minimizing collisions and
/// lookup latency. Default value (1.0) shows x2 slower work.
inline constexpr float CHUNKS_MAP_MAX_LOAD_FACTOR = 0.1f;
/// @brief chunk volume (count of voxels per Chunk)
inline constexpr int CHUNK_VOL = (CHUNK_W * CHUNK_H * CHUNK_D);

View File

@ -91,10 +91,18 @@ std::unique_ptr<Content> ContentBuilder::build() {
for (Block* def : blockDefsIndices) {
def->rt.pickingItem = content->items.require(def->pickingItem).rt.id;
def->rt.surfaceReplacement = content->blocks.require(def->surfaceReplacement).rt.id;
if (def->properties == nullptr) {
def->properties = dv::object();
def->properties["name"] = def->name;
}
}
for (ItemDef* def : itemDefsIndices) {
def->rt.placingBlock = content->blocks.require(def->placingBlock).rt.id;
if (def->properties == nullptr) {
def->properties = dv::object();
}
def->properties["name"] = def->name;
}
for (auto& [name, def] : content->generators.getDefs()) {

View File

@ -35,11 +35,13 @@ public:
: allNames(allNames), type(type) {
}
T& create(const std::string& id) {
T& create(const std::string& id, bool* created = nullptr) {
auto found = defs.find(id);
if (found != defs.end()) {
if (created) *created = false;
return *found->second;
}
if (created) *created = true;
checkIdentifier(id);
allNames[id] = type;
names.push_back(id);

View File

@ -187,11 +187,49 @@ static void perform_user_block_fields(
layout = StructLayout::create(fields);
}
static void process_method(
dv::value& properties,
const std::string& method,
const std::string& name,
const dv::value& value
) {
if (method == "append") {
if (!properties.has(name)) {
properties[name] = dv::list();
}
auto& list = properties[name];
if (value.isList()) {
for (const auto& item : value) {
list.add(item);
}
} else {
list.add(value);
}
} else {
throw std::runtime_error(
"unknown method " + method + " for " + name
);
}
}
void ContentLoader::loadBlock(
Block& def, const std::string& name, const fs::path& file
) {
auto root = files::read_json(file);
def.properties = root;
if (def.properties == nullptr) {
def.properties = dv::object();
def.properties["name"] = name;
}
for (auto& [key, value] : root.asObject()) {
auto pos = key.rfind('@');
if (pos == std::string::npos) {
def.properties[key] = value;
continue;
}
auto field = key.substr(0, pos);
auto suffix = key.substr(pos + 1);
process_method(def.properties, suffix, field, value);
}
if (root.has("parent")) {
const auto& parentName = root["parent"].asString();
@ -492,7 +530,8 @@ void ContentLoader::loadBlock(
if (fs::exists(configFile)) loadBlock(def, full, configFile);
if (!def.hidden) {
auto& item = builder.items.create(full + BLOCK_ITEM_SUFFIX);
bool created;
auto& item = builder.items.create(full + BLOCK_ITEM_SUFFIX, &created);
item.generated = true;
item.caption = def.caption;
item.iconType = ItemIconType::BLOCK;
@ -502,7 +541,7 @@ void ContentLoader::loadBlock(
for (uint j = 0; j < 4; j++) {
item.emission[j] = def.emission[j];
}
stats->totalItems++;
stats->totalItems += created;
}
}
@ -564,9 +603,10 @@ void ContentLoader::loadContent(const dv::value& root) {
if (parent.empty() || builder.blocks.get(parent)) {
// No dependency or dependency already loaded/exists in another
// content pack
auto& def = builder.blocks.create(full);
bool created;
auto& def = builder.blocks.create(full, &created);
loadBlock(def, full, name);
stats->totalBlocks++;
stats->totalBlocks += created;
} else {
// Dependency not loaded yet, add to pending items
pendingDefs.emplace_back(full, name);
@ -583,9 +623,10 @@ void ContentLoader::loadContent(const dv::value& root) {
if (builder.blocks.get(parent)) {
// Dependency resolved or parent exists in another pack,
// load the item
auto& def = builder.blocks.create(it->first);
bool created;
auto& def = builder.blocks.create(it->first, &created);
loadBlock(def, it->first, it->second);
stats->totalBlocks++;
stats->totalBlocks += created;
it = pendingDefs.erase(it); // Remove resolved item
progressMade = true;
} else {
@ -609,9 +650,10 @@ void ContentLoader::loadContent(const dv::value& root) {
if (parent.empty() || builder.items.get(parent)) {
// No dependency or dependency already loaded/exists in another
// content pack
auto& def = builder.items.create(full);
bool created;
auto& def = builder.items.create(full, &created);
loadItem(def, full, name);
stats->totalItems++;
stats->totalItems += created;
} else {
// Dependency not loaded yet, add to pending items
pendingDefs.emplace_back(full, name);
@ -628,9 +670,10 @@ void ContentLoader::loadContent(const dv::value& root) {
if (builder.items.get(parent)) {
// Dependency resolved or parent exists in another pack,
// load the item
auto& def = builder.items.create(it->first);
bool created;
auto& def = builder.items.create(it->first, &created);
loadItem(def, it->first, it->second);
stats->totalItems++;
stats->totalItems += created;
it = pendingDefs.erase(it); // Remove resolved item
progressMade = true;
} else {
@ -654,9 +697,10 @@ void ContentLoader::loadContent(const dv::value& root) {
if (parent.empty() || builder.entities.get(parent)) {
// No dependency or dependency already loaded/exists in another
// content pack
auto& def = builder.entities.create(full);
bool created;
auto& def = builder.entities.create(full, &created);
loadEntity(def, full, name);
stats->totalEntities++;
stats->totalEntities += created;
} else {
// Dependency not loaded yet, add to pending items
pendingDefs.emplace_back(full, name);
@ -673,9 +717,10 @@ void ContentLoader::loadContent(const dv::value& root) {
if (builder.entities.get(parent)) {
// Dependency resolved or parent exists in another pack,
// load the item
auto& def = builder.entities.create(it->first);
bool created;
auto& def = builder.entities.create(it->first, &created);
loadEntity(def, it->first, it->second);
stats->totalEntities++;
stats->totalEntities += created;
it = pendingDefs.erase(it); // Remove resolved item
progressMade = true;
} else {

View File

@ -13,9 +13,9 @@
namespace fs = std::filesystem;
ContentPack ContentPack::createCore(const EnginePaths* paths) {
ContentPack ContentPack::createCore(const EnginePaths& paths) {
return ContentPack {
"core", "Core", ENGINE_VERSION_STRING, "", "", paths->getResourcesFolder(), {}
"core", "Core", ENGINE_VERSION_STRING, "", "", paths.getResourcesFolder(), "res:", {}
};
}
@ -70,7 +70,7 @@ static void checkContentPackId(const std::string& id, const fs::path& folder) {
}
}
ContentPack ContentPack::read(const fs::path& folder) {
ContentPack ContentPack::read(const std::string& path, const fs::path& folder) {
auto root = files::read_json(folder / fs::path(PACKAGE_FILENAME));
ContentPack pack;
root.at("id").get(pack.id);
@ -90,6 +90,7 @@ ContentPack ContentPack::read(const fs::path& folder) {
root.at("description").get(pack.description);
root.at("source").get(pack.source);
pack.folder = folder;
pack.path = path;
if (auto found = root.at("dependencies")) {
const auto& dependencies = *found;
@ -123,17 +124,19 @@ ContentPack ContentPack::read(const fs::path& folder) {
}
void ContentPack::scanFolder(
const fs::path& folder, std::vector<ContentPack>& packs
const std::string& path, const fs::path& folder, std::vector<ContentPack>& packs
) {
if (!fs::is_directory(folder)) {
return;
}
for (const auto& entry : fs::directory_iterator(folder)) {
const fs::path& folder = entry.path();
if (!fs::is_directory(folder)) continue;
if (!is_pack(folder)) continue;
const fs::path& packFolder = entry.path();
if (!fs::is_directory(packFolder)) continue;
if (!is_pack(packFolder)) continue;
try {
packs.push_back(read(folder));
packs.push_back(
read(path + "/" + packFolder.filename().string(), packFolder)
);
} catch (const contentpack_error& err) {
std::cerr << "package.json error at " << err.getFolder().u8string();
std::cerr << ": " << err.what() << std::endl;
@ -146,9 +149,7 @@ void ContentPack::scanFolder(
std::vector<std::string> ContentPack::worldPacksList(const fs::path& folder) {
fs::path listfile = folder / fs::path("packs.list");
if (!fs::is_regular_file(listfile)) {
std::cerr << "warning: packs.list not found (will be created)";
std::cerr << std::endl;
files::write_string(listfile, "# autogenerated, do not modify\nbase\n");
throw std::runtime_error("missing file 'packs.list'");
}
return files::read_list(listfile);
}

View File

@ -10,18 +10,18 @@
class EnginePaths;
namespace fs = std::filesystem;
class contentpack_error : public std::runtime_error {
std::string packId;
fs::path folder;
std::filesystem::path folder;
public:
contentpack_error(
std::string packId, fs::path folder, const std::string& message
std::string packId,
std::filesystem::path folder,
const std::string& message
);
std::string getPackId() const;
fs::path getFolder() const;
std::filesystem::path getFolder() const;
};
enum class DependencyLevel {
@ -42,45 +42,52 @@ struct ContentPack {
std::string version = "0.0";
std::string creator = "";
std::string description = "no description";
fs::path folder;
std::filesystem::path folder;
std::string path;
std::vector<DependencyPack> dependencies;
std::string source = "";
fs::path getContentFile() const;
std::filesystem::path getContentFile() const;
static inline const std::string PACKAGE_FILENAME = "package.json";
static inline const std::string CONTENT_FILENAME = "content.json";
static inline const fs::path BLOCKS_FOLDER = "blocks";
static inline const fs::path ITEMS_FOLDER = "items";
static inline const fs::path ENTITIES_FOLDER = "entities";
static inline const fs::path GENERATORS_FOLDER = "generators";
static inline const std::filesystem::path BLOCKS_FOLDER = "blocks";
static inline const std::filesystem::path ITEMS_FOLDER = "items";
static inline const std::filesystem::path ENTITIES_FOLDER = "entities";
static inline const std::filesystem::path GENERATORS_FOLDER = "generators";
static const std::vector<std::string> RESERVED_NAMES;
static bool is_pack(const fs::path& folder);
static ContentPack read(const fs::path& folder);
static void scanFolder(
const fs::path& folder, std::vector<ContentPack>& packs
static bool is_pack(const std::filesystem::path& folder);
static ContentPack read(
const std::string& path, const std::filesystem::path& folder
);
static std::vector<std::string> worldPacksList(const fs::path& folder);
static void scanFolder(
const std::string& path,
const std::filesystem::path& folder,
std::vector<ContentPack>& packs
);
static fs::path findPack(
static std::vector<std::string> worldPacksList(
const std::filesystem::path& folder
);
static std::filesystem::path findPack(
const EnginePaths* paths,
const fs::path& worldDir,
const std::filesystem::path& worldDir,
const std::string& name
);
static ContentPack createCore(const EnginePaths*);
static ContentPack createCore(const EnginePaths&);
static inline fs::path getFolderFor(ContentType type) {
static inline std::filesystem::path getFolderFor(ContentType type) {
switch (type) {
case ContentType::BLOCK: return ContentPack::BLOCKS_FOLDER;
case ContentType::ITEM: return ContentPack::ITEMS_FOLDER;
case ContentType::ENTITY: return ContentPack::ENTITIES_FOLDER;
case ContentType::GENERATOR: return ContentPack::GENERATORS_FOLDER;
case ContentType::NONE: return fs::u8path("");
default: return fs::u8path("");
case ContentType::NONE: return std::filesystem::u8path("");
default: return std::filesystem::u8path("");
}
}
};
@ -95,12 +102,13 @@ struct ContentPackStats {
}
};
struct world_funcs_set {
bool onblockplaced : 1;
bool onblockreplaced : 1;
bool onblockbroken : 1;
bool onblockinteract : 1;
bool onplayertick : 1;
struct WorldFuncsSet {
bool onblockplaced;
bool onblockreplaced;
bool onblockbreaking;
bool onblockbroken;
bool onblockinteract;
bool onplayertick;
};
class ContentPackRuntime {
@ -108,7 +116,7 @@ class ContentPackRuntime {
ContentPackStats stats {};
scriptenv env;
public:
world_funcs_set worldfuncsset {};
WorldFuncsSet worldfuncsset {};
ContentPackRuntime(ContentPack info, scriptenv env);
~ContentPackRuntime();

View File

@ -7,7 +7,7 @@
PacksManager::PacksManager() = default;
void PacksManager::setSources(std::vector<fs::path> sources) {
void PacksManager::setSources(std::vector<std::pair<std::string, fs::path>> sources) {
this->sources = std::move(sources);
}
@ -15,8 +15,8 @@ void PacksManager::scan() {
packs.clear();
std::vector<ContentPack> packsList;
for (auto& folder : sources) {
ContentPack::scanFolder(folder, packsList);
for (auto& [path, folder] : sources) {
ContentPack::scanFolder(path, folder, packsList);
for (auto& pack : packsList) {
packs.try_emplace(pack.id, pack);
}
@ -116,7 +116,7 @@ static bool resolve_dependencies(
return satisfied;
}
std::vector<std::string> PacksManager::assembly(
std::vector<std::string> PacksManager::assemble(
const std::vector<std::string>& names
) const {
std::vector<std::string> allNames = names;

View File

@ -10,12 +10,12 @@ namespace fs = std::filesystem;
class PacksManager {
std::unordered_map<std::string, ContentPack> packs;
std::vector<fs::path> sources;
std::vector<std::pair<std::string, fs::path>> sources;
public:
PacksManager();
/// @brief Set content packs sources (search folders)
void setSources(std::vector<fs::path> sources);
void setSources(std::vector<std::pair<std::string, fs::path>> sources);
/// @brief Scan sources and collect all found packs excluding duplication.
/// Scanning order depends on sources order
@ -38,7 +38,7 @@ public:
/// @return resulting ordered vector of pack names
/// @throws contentpack_error if required dependency not found or
/// circular dependency detected
std::vector<std::string> assembly(const std::vector<std::string>& names
std::vector<std::string> assemble(const std::vector<std::string>& names
) const;
/// @brief Collect all pack names (identifiers) into a new vector

View File

@ -11,9 +11,9 @@
#include "voxels/Block.hpp"
// All in-game definitions (blocks, items, etc..)
void corecontent::setup(EnginePaths* paths, ContentBuilder* builder) {
void corecontent::setup(const EnginePaths& paths, ContentBuilder& builder) {
{
Block& block = builder->blocks.create(CORE_AIR);
Block& block = builder.blocks.create(CORE_AIR);
block.replaceable = true;
block.drawGroup = 1;
block.lightPassing = true;
@ -24,11 +24,11 @@ void corecontent::setup(EnginePaths* paths, ContentBuilder* builder) {
block.pickingItem = CORE_EMPTY;
}
{
ItemDef& item = builder->items.create(CORE_EMPTY);
ItemDef& item = builder.items.create(CORE_EMPTY);
item.iconType = ItemIconType::NONE;
}
auto bindsFile = paths->getResourcesFolder()/fs::path("bindings.toml");
auto bindsFile = paths.getResourcesFolder()/fs::path("bindings.toml");
if (fs::is_regular_file(bindsFile)) {
Events::loadBindings(
bindsFile.u8string(), files::read_string(bindsFile), BindType::BIND
@ -36,20 +36,20 @@ void corecontent::setup(EnginePaths* paths, ContentBuilder* builder) {
}
{
Block& block = builder->blocks.create(CORE_OBSTACLE);
Block& block = builder.blocks.create(CORE_OBSTACLE);
for (uint i = 0; i < 6; i++) {
block.textureFaces[i] = "obstacle";
}
block.hitboxes = {AABB()};
block.breakable = false;
ItemDef& item = builder->items.create(CORE_OBSTACLE+".item");
ItemDef& item = builder.items.create(CORE_OBSTACLE+".item");
item.iconType = ItemIconType::BLOCK;
item.icon = CORE_OBSTACLE;
item.placingBlock = CORE_OBSTACLE;
item.caption = block.caption;
}
{
Block& block = builder->blocks.create(CORE_STRUCT_AIR);
Block& block = builder.blocks.create(CORE_STRUCT_AIR);
for (uint i = 0; i < 6; i++) {
block.textureFaces[i] = "struct_air";
}
@ -58,7 +58,7 @@ void corecontent::setup(EnginePaths* paths, ContentBuilder* builder) {
block.lightPassing = true;
block.hitboxes = {AABB()};
block.obstacle = false;
ItemDef& item = builder->items.create(CORE_STRUCT_AIR+".item");
ItemDef& item = builder.items.create(CORE_STRUCT_AIR+".item");
item.iconType = ItemIconType::BLOCK;
item.icon = CORE_STRUCT_AIR;
item.placingBlock = CORE_STRUCT_AIR;

View File

@ -21,12 +21,9 @@ inline const std::string BIND_MOVE_CROUCH = "movement.crouch";
inline const std::string BIND_MOVE_CHEAT = "movement.cheat";
inline const std::string BIND_CAM_ZOOM = "camera.zoom";
inline const std::string BIND_CAM_MODE = "camera.mode";
inline const std::string BIND_PLAYER_NOCLIP = "player.noclip";
inline const std::string BIND_PLAYER_FLIGHT = "player.flight";
inline const std::string BIND_PLAYER_ATTACK = "player.attack";
inline const std::string BIND_PLAYER_DESTROY = "player.destroy";
inline const std::string BIND_PLAYER_BUILD = "player.build";
inline const std::string BIND_PLAYER_PICK = "player.pick";
inline const std::string BIND_PLAYER_FAST_INTERACTOIN =
"player.fast_interaction";
inline const std::string BIND_HUD_INVENTORY = "hud.inventory";
@ -35,5 +32,5 @@ class EnginePaths;
class ContentBuilder;
namespace corecontent {
void setup(EnginePaths* paths, ContentBuilder* builder);
void setup(const EnginePaths& paths, ContentBuilder& builder);
}

View File

@ -95,8 +95,8 @@ static inline FieldIncapatibilityType checkIncapatibility(
static inline integer_t clamp_value(integer_t value, FieldType type) {
auto typesize = sizeof_type(type) * CHAR_BIT;
integer_t minval = -(1 << (typesize-1));
integer_t maxval = (1 << (typesize-1))-1;
integer_t minval = -(1LL << (typesize-1));
integer_t maxval = (1LL << (typesize-1))-1;
return std::min(maxval, std::max(minval, value));
}

View File

@ -23,10 +23,16 @@ Logger::Logger(std::string name) : name(std::move(name)) {
void Logger::log(
LogLevel level, const std::string& name, const std::string& message
) {
if (level == LogLevel::print) {
std::cout << "[" << name << "] " << message << std::endl;
return;
}
using namespace std::chrono;
std::stringstream ss;
switch (level) {
case LogLevel::print:
case LogLevel::debug:
#ifdef NDEBUG
return;

View File

@ -5,7 +5,7 @@
#include <sstream>
namespace debug {
enum class LogLevel { debug, info, warning, error };
enum class LogLevel { print, debug, info, warning, error };
class Logger;
@ -60,5 +60,10 @@ namespace debug {
LogMessage warning() {
return LogMessage(this, LogLevel::warning);
}
/// @brief Print-debugging tool (printed without header)
LogMessage print() {
return LogMessage(this, LogLevel::print);
}
};
}

View File

@ -8,6 +8,8 @@ using runnable = std::function<void()>;
template<class T> using supplier = std::function<T()>;
template<class T> using consumer = std::function<void(T)>;
using KeyCallback = std::function<bool()>;
// data sources
using wstringsupplier = std::function<std::wstring()>;
using doublesupplier = std::function<double()>;

View File

@ -32,7 +32,6 @@ static std::unique_ptr<FontStylesScheme> build_styles(
continue;
}
if (token.start.pos > offset) {
int n = token.start.pos - offset;
styles.map.insert(styles.map.end(), token.start.pos - offset, 0);
}
offset = token.end.pos;

View File

@ -1,6 +1,8 @@
#include "engine.hpp"
#include "Engine.hpp"
#ifndef GLEW_STATIC
#define GLEW_STATIC
#endif
#include "debug/Logger.hpp"
#include "assets/AssetsLoader.hpp"
@ -15,13 +17,10 @@
#include "content/ContentLoader.hpp"
#include "core_defs.hpp"
#include "files/files.hpp"
#include "files/settings_io.hpp"
#include "frontend/locale.hpp"
#include "frontend/menu.hpp"
#include "frontend/screens/Screen.hpp"
#include "frontend/screens/MenuScreen.hpp"
#include "graphics/render/ModelsGenerator.hpp"
#include "graphics/core/Batch2D.hpp"
#include "graphics/core/DrawContext.hpp"
#include "graphics/core/ImageData.hpp"
#include "graphics/core/Shader.hpp"
@ -30,6 +29,7 @@
#include "logic/EngineController.hpp"
#include "logic/CommandsInterpreter.hpp"
#include "logic/scripting/scripting.hpp"
#include "logic/scripting/scripting_hud.hpp"
#include "network/Network.hpp"
#include "util/listutil.hpp"
#include "util/platform.hpp"
@ -37,7 +37,9 @@
#include "window/Events.hpp"
#include "window/input.hpp"
#include "window/Window.hpp"
#include "settings.hpp"
#include "world/Level.hpp"
#include "Mainloop.hpp"
#include "ServerMainloop.hpp"
#include <iostream>
#include <assert.h>
@ -71,52 +73,69 @@ static std::unique_ptr<ImageData> load_icon(const fs::path& resdir) {
return nullptr;
}
Engine::Engine(EngineSettings& settings, SettingsHandler& settingsHandler, EnginePaths* paths)
: settings(settings), settingsHandler(settingsHandler), paths(paths),
Engine::Engine(CoreParameters coreParameters)
: params(std::move(coreParameters)),
settings(),
settingsHandler({settings}),
interpreter(std::make_unique<cmd::CommandsInterpreter>()),
network(network::Network::create(settings.network))
{
paths->prepare();
network(network::Network::create(settings.network)) {
logger.info() << "engine version: " << ENGINE_VERSION_STRING;
if (params.headless) {
logger.info() << "headless mode is enabled";
}
paths.setResourcesFolder(params.resFolder);
paths.setUserFilesFolder(params.userFolder);
paths.prepare();
if (!params.scriptFile.empty()) {
paths.setScriptFolder(params.scriptFile.parent_path());
}
loadSettings();
auto resdir = paths->getResourcesFolder();
auto resdir = paths.getResourcesFolder();
controller = std::make_unique<EngineController>(this);
if (Window::initialize(&this->settings.display)){
throw initialize_error("could not initialize window");
controller = std::make_unique<EngineController>(*this);
if (!params.headless) {
if (Window::initialize(&settings.display)){
throw initialize_error("could not initialize window");
}
time.set(Window::time());
if (auto icon = load_icon(resdir)) {
icon->flipY();
Window::setIcon(icon.get());
}
loadControls();
gui = std::make_unique<gui::GUI>();
if (ENGINE_DEBUG_BUILD) {
menus::create_version_label(*this);
}
}
if (auto icon = load_icon(resdir)) {
icon->flipY();
Window::setIcon(icon.get());
}
loadControls();
audio::initialize(settings.audio.enabled.get());
audio::initialize(settings.audio.enabled.get() && !params.headless);
create_channel(this, "master", settings.audio.volumeMaster);
create_channel(this, "regular", settings.audio.volumeRegular);
create_channel(this, "music", settings.audio.volumeMusic);
create_channel(this, "ambient", settings.audio.volumeAmbient);
create_channel(this, "ui", settings.audio.volumeUI);
gui = std::make_unique<gui::GUI>();
if (settings.ui.language.get() == "auto") {
bool langNotSet = settings.ui.language.get() == "auto";
if (langNotSet) {
settings.ui.language.set(langs::locale_by_envlocale(
platform::detect_locale(),
paths->getResourcesFolder()
paths.getResourcesFolder()
));
}
if (ENGINE_DEBUG_BUILD) {
menus::create_version_label(this);
scripting::initialize(this);
if (!isHeadless()) {
gui->setPageLoader(scripting::create_page_loader());
}
keepAlive(settings.ui.language.observe([=](auto lang) {
keepAlive(settings.ui.language.observe([this](auto lang) {
setLanguage(lang);
}, true));
scripting::initialize(this);
basePacks = files::read_list(resdir/fs::path("config/builtins.list"));
}
void Engine::loadSettings() {
fs::path settings_file = paths->getSettingsFile();
fs::path settings_file = paths.getSettingsFile();
if (fs::is_regular_file(settings_file)) {
logger.info() << "loading settings";
std::string text = files::read_string(settings_file);
@ -130,7 +149,7 @@ void Engine::loadSettings() {
}
void Engine::loadControls() {
fs::path controls_file = paths->getControlsFile();
fs::path controls_file = paths.getControlsFile();
if (fs::is_regular_file(controls_file)) {
logger.info() << "loading controls";
std::string text = files::read_string(controls_file);
@ -143,13 +162,6 @@ void Engine::onAssetsLoaded() {
gui->onAssetsLoad(assets.get());
}
void Engine::updateTimers() {
frame++;
double currentTime = Window::time();
delta = currentTime - lastTime;
lastTime = currentTime;
}
void Engine::updateHotkeys() {
if (Events::jpressed(keycode::F2)) {
saveScreenshot();
@ -162,67 +174,58 @@ void Engine::updateHotkeys() {
void Engine::saveScreenshot() {
auto image = Window::takeScreenshot();
image->flipY();
fs::path filename = paths->getNewScreenshotFile("png");
fs::path filename = paths.getNewScreenshotFile("png");
imageio::write(filename.string(), image.get());
logger.info() << "saved screenshot as " << filename.u8string();
}
void Engine::mainloop() {
logger.info() << "starting menu screen";
setScreen(std::make_shared<MenuScreen>(this));
Batch2D batch(1024);
lastTime = Window::time();
logger.info() << "engine started";
while (!Window::isShouldClose()){
assert(screen != nullptr);
updateTimers();
updateHotkeys();
audio::update(delta);
gui->act(delta, Viewport(Window::width, Window::height));
screen->update(delta);
if (!Window::isIconified()) {
renderFrame(batch);
}
Window::setFramerate(
Window::isIconified() && settings.display.limitFpsIconified.get()
? 20
: settings.display.framerate.get()
);
network->update();
processPostRunnables();
Window::swapBuffers();
Events::pollEvents();
void Engine::run() {
if (params.headless) {
ServerMainloop(*this).run();
} else {
Mainloop(*this).run();
}
}
void Engine::renderFrame(Batch2D& batch) {
screen->draw(delta);
void Engine::postUpdate() {
network->update();
postRunnables.run();
scripting::process_post_runnables();
}
void Engine::updateFrontend() {
double delta = time.getDelta();
updateHotkeys();
audio::update(delta);
gui->act(delta, Viewport(Window::width, Window::height));
screen->update(delta);
}
void Engine::nextFrame() {
Window::setFramerate(
Window::isIconified() && settings.display.limitFpsIconified.get()
? 20
: settings.display.framerate.get()
);
Window::swapBuffers();
Events::pollEvents();
}
void Engine::renderFrame() {
screen->draw(time.getDelta());
Viewport viewport(Window::width, Window::height);
DrawContext ctx(nullptr, viewport, &batch);
DrawContext ctx(nullptr, viewport, nullptr);
gui->draw(ctx, *assets);
}
void Engine::processPostRunnables() {
std::lock_guard<std::recursive_mutex> lock(postRunnablesMutex);
while (!postRunnables.empty()) {
postRunnables.front()();
postRunnables.pop();
}
scripting::process_post_runnables();
}
void Engine::saveSettings() {
logger.info() << "saving settings";
files::write_string(paths->getSettingsFile(), toml::stringify(settingsHandler));
logger.info() << "saving bindings";
files::write_string(paths->getControlsFile(), Events::writeBindings());
files::write_string(paths.getSettingsFile(), toml::stringify(settingsHandler));
if (!params.headless) {
logger.info() << "saving bindings";
files::write_string(paths.getControlsFile(), Events::writeBindings());
}
}
Engine::~Engine() {
@ -235,13 +238,19 @@ Engine::~Engine() {
content.reset();
assets.reset();
interpreter.reset();
gui.reset();
logger.info() << "gui finished";
if (gui) {
gui.reset();
logger.info() << "gui finished";
}
audio::close();
network.reset();
clearKeepedObjects();
scripting::close();
logger.info() << "scripting finished";
Window::terminate();
if (!params.headless) {
Window::terminate();
logger.info() << "window closed";
}
logger.info() << "engine finished";
}
@ -256,13 +265,17 @@ cmd::CommandsInterpreter* Engine::getCommandsInterpreter() {
PacksManager Engine::createPacksManager(const fs::path& worldFolder) {
PacksManager manager;
manager.setSources({
worldFolder/fs::path("content"),
paths->getUserFilesFolder()/fs::path("content"),
paths->getResourcesFolder()/fs::path("content")
{"world:content", worldFolder.empty() ? worldFolder : worldFolder/fs::path("content")},
{"user:content", paths.getUserFilesFolder()/fs::path("content")},
{"res:content", paths.getResourcesFolder()/fs::path("content")}
});
return manager;
}
void Engine::setLevelConsumer(consumer<std::unique_ptr<Level>> levelConsumer) {
this->levelConsumer = std::move(levelConsumer);
}
void Engine::loadAssets() {
logger.info() << "loading assets";
Shader::preprocessor->setPaths(resPaths.get());
@ -278,42 +291,36 @@ void Engine::loadAssets() {
auto task = loader.startTask([=](){});
task->waitForEnd();
} else {
try {
while (loader.hasNext()) {
loader.loadNext();
}
} catch (const assetload::error& err) {
new_assets.reset();
throw;
while (loader.hasNext()) {
loader.loadNext();
}
}
assets = std::move(new_assets);
if (content) {
for (auto& [name, def] : content->blocks.getDefs()) {
if (def->model == BlockModel::custom) {
if (def->modelName.empty()) {
assets->store(
std::make_unique<model::Model>(
ModelsGenerator::loadCustomBlockModel(
def->customModelRaw, *assets, !def->shadeless
)
),
name + ".model"
);
def->modelName = def->name + ".model";
}
}
}
for (auto& [name, def] : content->items.getDefs()) {
if (content == nullptr) {
return;
}
for (auto& [name, def] : content->blocks.getDefs()) {
if (def->model == BlockModel::custom && def->modelName.empty()) {
assets->store(
std::make_unique<model::Model>(
ModelsGenerator::generate(*def, *content, *assets)
ModelsGenerator::loadCustomBlockModel(
def->customModelRaw, *assets, !def->shadeless
)
),
name + ".model"
);
def->modelName = def->name + ".model";
}
}
for (auto& [name, def] : content->items.getDefs()) {
assets->store(
std::make_unique<model::Model>(
ModelsGenerator::generate(*def, *content, *assets)
),
name + ".model"
);
}
}
static void load_configs(const fs::path& root) {
@ -329,7 +336,7 @@ static void load_configs(const fs::path& root) {
void Engine::loadContent() {
scripting::cleanup();
auto resdir = paths->getResourcesFolder();
auto resdir = paths.getResourcesFolder();
std::vector<std::string> names;
for (auto& pack : contentPacks) {
@ -337,12 +344,12 @@ void Engine::loadContent() {
}
ContentBuilder contentBuilder;
corecontent::setup(paths, &contentBuilder);
corecontent::setup(paths, contentBuilder);
paths->setContentPacks(&contentPacks);
PacksManager manager = createPacksManager(paths->getCurrentWorldFolder());
paths.setContentPacks(&contentPacks);
PacksManager manager = createPacksManager(paths.getCurrentWorldFolder());
manager.scan();
names = manager.assembly(names);
names = manager.assemble(names);
contentPacks = manager.getAll(names);
auto corePack = ContentPack::createCore(paths);
@ -372,13 +379,15 @@ void Engine::loadContent() {
ContentLoader::loadScripts(*content);
langs::setup(resdir, langs::current->getId(), contentPacks);
loadAssets();
onAssetsLoaded();
if (!isHeadless()) {
loadAssets();
onAssetsLoaded();
}
}
void Engine::resetContent() {
scripting::cleanup();
auto resdir = paths->getResourcesFolder();
auto resdir = paths.getResourcesFolder();
std::vector<PathsRoot> resRoots;
{
auto pack = ContentPack::createCore(paths);
@ -395,8 +404,10 @@ void Engine::resetContent() {
content.reset();
langs::setup(resdir, langs::current->getId(), contentPacks);
loadAssets();
onAssetsLoaded();
if (!isHeadless()) {
loadAssets();
onAssetsLoaded();
}
contentPacks = manager.getAll(basePacks);
}
@ -405,26 +416,23 @@ void Engine::loadWorldContent(const fs::path& folder) {
contentPacks.clear();
auto packNames = ContentPack::worldPacksList(folder);
PacksManager manager;
manager.setSources({
folder/fs::path("content"),
paths->getUserFilesFolder()/fs::path("content"),
paths->getResourcesFolder()/fs::path("content")
});
manager.setSources(
{{"world:content",
folder.empty() ? folder : folder / fs::path("content")},
{"user:content", paths.getUserFilesFolder() / fs::path("content")},
{"res:content", paths.getResourcesFolder() / fs::path("content")}}
);
manager.scan();
contentPacks = manager.getAll(manager.assembly(packNames));
paths->setCurrentWorldFolder(folder);
contentPacks = manager.getAll(manager.assemble(packNames));
paths.setCurrentWorldFolder(folder);
loadContent();
}
void Engine::loadAllPacks() {
PacksManager manager = createPacksManager(paths->getCurrentWorldFolder());
PacksManager manager = createPacksManager(paths.getCurrentWorldFolder());
manager.scan();
auto allnames = manager.getAllNames();
contentPacks = manager.getAll(manager.assembly(allnames));
}
double Engine::getDelta() const {
return delta;
contentPacks = manager.getAll(manager.assemble(allnames));
}
void Engine::setScreen(std::shared_ptr<Screen> screen) {
@ -435,8 +443,28 @@ void Engine::setScreen(std::shared_ptr<Screen> screen) {
}
void Engine::setLanguage(std::string locale) {
langs::setup(paths->getResourcesFolder(), std::move(locale), contentPacks);
gui->getMenu()->setPageLoader(menus::create_page_loader(this));
langs::setup(paths.getResourcesFolder(), std::move(locale), contentPacks);
}
void Engine::onWorldOpen(std::unique_ptr<Level> level) {
logger.info() << "world open";
levelConsumer(std::move(level));
}
void Engine::onWorldClosed() {
logger.info() << "world closed";
levelConsumer(nullptr);
}
void Engine::quit() {
quitSignal = true;
if (!isHeadless()) {
Window::setShouldClose(true);
}
}
bool Engine::isQuitSignal() const {
return quitSignal;
}
gui::GUI* Engine::getGUI() {
@ -469,7 +497,7 @@ std::vector<std::string>& Engine::getBasePacks() {
return basePacks;
}
EnginePaths* Engine::getPaths() {
EnginePaths& Engine::getPaths() {
return paths;
}
@ -481,11 +509,6 @@ std::shared_ptr<Screen> Engine::getScreen() {
return screen;
}
void Engine::postRunnable(const runnable& callback) {
std::lock_guard<std::recursive_mutex> lock(postRunnablesMutex);
postRunnables.push(callback);
}
SettingsHandler& Engine::getSettingsHandler() {
return settingsHandler;
}
@ -493,3 +516,15 @@ SettingsHandler& Engine::getSettingsHandler() {
network::Network& Engine::getNetwork() {
return *network;
}
Time& Engine::getTime() {
return time;
}
const CoreParameters& Engine::getCoreParameters() const {
return params;
}
bool Engine::isHeadless() const {
return params.headless;
}

View File

@ -2,32 +2,32 @@
#include "delegates.hpp"
#include "typedefs.hpp"
#include "settings.hpp"
#include "assets/Assets.hpp"
#include "content/content_fwd.hpp"
#include "content/ContentPack.hpp"
#include "content/PacksManager.hpp"
#include "files/engine_paths.hpp"
#include "files/settings_io.hpp"
#include "util/ObjectsKeeper.hpp"
#include "PostRunnables.hpp"
#include "Time.hpp"
#include <filesystem>
#include <memory>
#include <queue>
#include <stdexcept>
#include <string>
#include <vector>
#include <mutex>
class Level;
class Screen;
class EnginePaths;
class ResPaths;
class Batch2D;
class EngineController;
class SettingsHandler;
struct EngineSettings;
namespace fs = std::filesystem;
namespace gui {
class GUI;
}
@ -45,44 +45,52 @@ public:
initialize_error(const std::string& message) : std::runtime_error(message) {}
};
struct CoreParameters {
bool headless = false;
bool testMode = false;
std::filesystem::path resFolder {"res"};
std::filesystem::path userFolder {"."};
std::filesystem::path scriptFile;
};
class Engine : public util::ObjectsKeeper {
EngineSettings& settings;
SettingsHandler& settingsHandler;
EnginePaths* paths;
CoreParameters params;
EngineSettings settings;
SettingsHandler settingsHandler;
EnginePaths paths;
std::unique_ptr<Assets> assets;
std::shared_ptr<Screen> screen;
std::vector<ContentPack> contentPacks;
std::unique_ptr<Content> content;
std::unique_ptr<ResPaths> resPaths;
std::queue<runnable> postRunnables;
std::recursive_mutex postRunnablesMutex;
std::unique_ptr<EngineController> controller;
std::unique_ptr<cmd::CommandsInterpreter> interpreter;
std::unique_ptr<network::Network> network;
std::vector<std::string> basePacks;
uint64_t frame = 0;
double lastTime = 0.0;
double delta = 0.0;
std::unique_ptr<gui::GUI> gui;
PostRunnables postRunnables;
Time time;
consumer<std::unique_ptr<Level>> levelConsumer;
bool quitSignal = false;
void loadControls();
void loadSettings();
void saveSettings();
void updateTimers();
void updateHotkeys();
void renderFrame(Batch2D& batch);
void processPostRunnables();
void loadAssets();
public:
Engine(EngineSettings& settings, SettingsHandler& settingsHandler, EnginePaths* paths);
Engine(CoreParameters coreParameters);
~Engine();
/// @brief Start main engine input/update/render loop.
/// Automatically sets MenuScreen
void mainloop();
/// @brief Start the engine
void run();
void postUpdate();
void updateFrontend();
void renderFrame();
void nextFrame();
/// @brief Called after assets loading when all engine systems are initialized
void onAssetsLoaded();
@ -100,6 +108,7 @@ public:
/// @brief Load all selected content-packs and reload assets
void loadContent();
/// @brief Reset content to base packs list
void resetContent();
/// @brief Collect world content-packs and load content
@ -110,9 +119,6 @@ public:
/// @brief Collect all available content-packs from res/content
void loadAllPacks();
/// @brief Get current frame delta-time
double getDelta() const;
/// @brief Get active assets storage instance
Assets* getAssets();
@ -123,11 +129,18 @@ public:
EngineSettings& getSettings();
/// @brief Get engine filesystem paths source
EnginePaths* getPaths();
EnginePaths& getPaths();
/// @brief Get engine resource paths controller
ResPaths* getResPaths();
void onWorldOpen(std::unique_ptr<Level> level);
void onWorldClosed();
void quit();
bool isQuitSignal() const;
/// @brief Get current Content instance
const Content* getContent() const;
@ -142,7 +155,9 @@ public:
std::shared_ptr<Screen> getScreen();
/// @brief Enqueue function call to the end of current frame in draw thread
void postRunnable(const runnable& callback);
void postRunnable(const runnable& callback) {
postRunnables.postRunnable(callback);
}
void saveScreenshot();
@ -151,7 +166,15 @@ public:
PacksManager createPacksManager(const fs::path& worldFolder);
void setLevelConsumer(consumer<std::unique_ptr<Level>> levelConsumer);
SettingsHandler& getSettingsHandler();
network::Network& getNetwork();
Time& getTime();
const CoreParameters& getCoreParameters() const;
bool isHeadless() const;
};

43
src/engine/Mainloop.cpp Normal file
View File

@ -0,0 +1,43 @@
#include "Mainloop.hpp"
#include "Engine.hpp"
#include "debug/Logger.hpp"
#include "frontend/screens/MenuScreen.hpp"
#include "frontend/screens/LevelScreen.hpp"
#include "window/Window.hpp"
#include "world/Level.hpp"
static debug::Logger logger("mainloop");
Mainloop::Mainloop(Engine& engine) : engine(engine) {
}
void Mainloop::run() {
auto& time = engine.getTime();
engine.setLevelConsumer([this](auto level) {
if (level == nullptr) {
// destroy LevelScreen and run quit callbacks
engine.setScreen(nullptr);
// create and go to menu screen
engine.setScreen(std::make_shared<MenuScreen>(engine));
} else {
engine.setScreen(std::make_shared<LevelScreen>(engine, std::move(level)));
}
});
logger.info() << "starting menu screen";
engine.setScreen(std::make_shared<MenuScreen>(engine));
logger.info() << "main loop started";
while (!Window::isShouldClose()){
time.update(Window::time());
engine.updateFrontend();
if (!Window::isIconified()) {
engine.renderFrame();
}
engine.postUpdate();
engine.nextFrame();
}
logger.info() << "main loop stopped";
}

11
src/engine/Mainloop.hpp Normal file
View File

@ -0,0 +1,11 @@
#pragma once
class Engine;
class Mainloop {
Engine& engine;
public:
Mainloop(Engine& engine);
void run();
};

View File

@ -0,0 +1,29 @@
#pragma once
#include <queue>
#include <mutex>
#include "delegates.hpp"
class PostRunnables {
std::queue<runnable> runnables;
std::recursive_mutex mutex;
public:
void postRunnable(runnable task) {
std::lock_guard<std::recursive_mutex> lock(mutex);
runnables.push(std::move(task));
}
void run() {
std::queue<runnable> tasksToRun;
{
std::lock_guard<std::recursive_mutex> lock(mutex);
std::swap(tasksToRun, runnables);
}
while (!tasksToRun.empty()) {
auto& task = tasksToRun.front();
task();
tasksToRun.pop();
}
}
};

View File

@ -0,0 +1,86 @@
#include "ServerMainloop.hpp"
#include "Engine.hpp"
#include "logic/scripting/scripting.hpp"
#include "logic/LevelController.hpp"
#include "interfaces/Process.hpp"
#include "debug/Logger.hpp"
#include "world/Level.hpp"
#include "world/World.hpp"
#include "util/platform.hpp"
#include <chrono>
using namespace std::chrono;
static debug::Logger logger("mainloop");
inline constexpr int TPS = 20;
ServerMainloop::ServerMainloop(Engine& engine) : engine(engine) {
}
ServerMainloop::~ServerMainloop() = default;
void ServerMainloop::run() {
const auto& coreParams = engine.getCoreParameters();
auto& time = engine.getTime();
if (coreParams.scriptFile.empty()) {
logger.info() << "nothing to do";
return;
}
engine.setLevelConsumer([this](auto level) {
setLevel(std::move(level));
});
logger.info() << "starting test " << coreParams.scriptFile;
auto process = scripting::start_coroutine(coreParams.scriptFile);
double targetDelta = 1.0 / static_cast<double>(TPS);
double delta = targetDelta;
auto begin = system_clock::now();
auto startupTime = begin;
while (process->isActive()) {
if (engine.isQuitSignal()) {
process->terminate();
logger.info() << "script has been terminated due to quit signal";
break;
}
if (coreParams.testMode) {
time.step(delta);
} else {
auto now = system_clock::now();
time.update(
duration_cast<microseconds>(now - startupTime).count() / 1e6);
delta = time.getDelta();
}
process->update();
if (controller) {
controller->getLevel()->getWorld()->updateTimers(delta);
controller->update(glm::min(delta, 0.2), false);
}
engine.postUpdate();
if (!coreParams.testMode) {
auto end = system_clock::now();
platform::sleep(targetDelta * 1000 -
duration_cast<microseconds>(end - begin).count() / 1000);
begin = system_clock::now();
}
}
logger.info() << "test finished";
}
void ServerMainloop::setLevel(std::unique_ptr<Level> level) {
if (level == nullptr) {
controller->onWorldQuit();
engine.getPaths().setCurrentWorldFolder(fs::path());
controller = nullptr;
} else {
controller = std::make_unique<LevelController>(
&engine, std::move(level), nullptr
);
}
}

View File

@ -0,0 +1,19 @@
#pragma once
#include <memory>
class Level;
class LevelController;
class Engine;
class ServerMainloop {
Engine& engine;
std::unique_ptr<LevelController> controller;
public:
ServerMainloop(Engine& engine);
~ServerMainloop();
void run();
void setLevel(std::unique_ptr<Level> level);
};

35
src/engine/Time.hpp Normal file
View File

@ -0,0 +1,35 @@
#pragma once
#include <stdint.h>
class Time {
uint64_t frame = 0;
double lastTime = 0.0;
double delta = 0.0;
public:
Time() {}
void update(double currentTime) {
frame++;
delta = currentTime - lastTime;
lastTime = currentTime;
}
void step(double delta) {
frame++;
lastTime += delta;
this->delta = delta;
}
void set(double currentTime) {
lastTime = currentTime;
}
double getDelta() const {
return delta;
}
double getTime() const {
return lastTime;
}
};

View File

@ -159,9 +159,9 @@ std::optional<WorldInfo> WorldFiles::readWorldInfo() {
}
static void read_resources_data(
const Content* content, const dv::value& list, ResourceType type
const Content& content, const dv::value& list, ResourceType type
) {
const auto& indices = content->getIndices(type);
const auto& indices = content.getIndices(type);
for (size_t i = 0; i < list.size(); i++) {
auto& map = list[i];
const auto& name = map["name"].asString();
@ -174,7 +174,7 @@ static void read_resources_data(
}
}
bool WorldFiles::readResourcesData(const Content* content) {
bool WorldFiles::readResourcesData(const Content& content) {
fs::path file = getResourcesFile();
if (!fs::is_regular_file(file)) {
logger.warning() << "resources.json does not exists";

View File

@ -49,7 +49,7 @@ public:
void createDirectories();
std::optional<WorldInfo> readWorldInfo();
bool readResourcesData(const Content* content);
bool readResourcesData(const Content& content);
static void createContentIndicesCache(
const ContentIndices* indices, dv::value& root

View File

@ -48,6 +48,18 @@ static std::filesystem::path toCanonic(std::filesystem::path path) {
}
void EnginePaths::prepare() {
if (!fs::is_directory(resourcesFolder)) {
throw std::runtime_error(
resourcesFolder.u8string() + " is not a directory"
);
}
if (!fs::is_directory(userFilesFolder)) {
fs::create_directories(userFilesFolder);
}
logger.info() << "resources folder: " << fs::canonical(resourcesFolder).u8string();
logger.info() << "user files folder: " << fs::canonical(userFilesFolder).u8string();
auto contentFolder = userFilesFolder / CONTENT_FOLDER;
if (!fs::is_directory(contentFolder)) {
fs::create_directories(contentFolder);
@ -120,7 +132,7 @@ std::filesystem::path EnginePaths::getSettingsFile() const {
return userFilesFolder / SETTINGS_FILE;
}
std::vector<std::filesystem::path> EnginePaths::scanForWorlds() {
std::vector<std::filesystem::path> EnginePaths::scanForWorlds() const {
std::vector<std::filesystem::path> folders;
auto folder = getWorldsFolder();
@ -157,6 +169,10 @@ void EnginePaths::setResourcesFolder(std::filesystem::path folder) {
this->resourcesFolder = std::move(folder);
}
void EnginePaths::setScriptFolder(std::filesystem::path folder) {
this->scriptFolder = std::move(folder);
}
void EnginePaths::setCurrentWorldFolder(std::filesystem::path folder) {
this->currentWorldFolder = std::move(folder);
}
@ -177,7 +193,7 @@ std::tuple<std::string, std::string> EnginePaths::parsePath(std::string_view pat
std::filesystem::path EnginePaths::resolve(
const std::string& path, bool throwErr
) {
) const {
auto [prefix, filename] = EnginePaths::parsePath(path);
if (prefix.empty()) {
throw files_access_error("no entry point specified");
@ -199,7 +215,9 @@ std::filesystem::path EnginePaths::resolve(
if (prefix == "export") {
return userFilesFolder / EXPORT_FOLDER / fs::u8path(filename);
}
if (prefix == "script" && scriptFolder) {
return scriptFolder.value() / fs::u8path(filename);
}
if (contentPacks) {
for (auto& pack : *contentPacks) {
if (pack.id == prefix) {

View File

@ -2,6 +2,7 @@
#include <filesystem>
#include <stdexcept>
#include <optional>
#include <string>
#include <vector>
#include <tuple>
@ -26,6 +27,8 @@ public:
void setResourcesFolder(std::filesystem::path folder);
std::filesystem::path getResourcesFolder() const;
void setScriptFolder(std::filesystem::path folder);
std::filesystem::path getWorldFolderByName(const std::string& name);
std::filesystem::path getWorldsFolder() const;
std::filesystem::path getConfigFolder() const;
@ -39,9 +42,9 @@ public:
void setContentPacks(std::vector<ContentPack>* contentPacks);
std::vector<std::filesystem::path> scanForWorlds();
std::vector<std::filesystem::path> scanForWorlds() const;
std::filesystem::path resolve(const std::string& path, bool throwErr = true);
std::filesystem::path resolve(const std::string& path, bool throwErr = true) const;
static std::tuple<std::string, std::string> parsePath(std::string_view view);
@ -51,6 +54,7 @@ private:
std::filesystem::path userFilesFolder {"."};
std::filesystem::path resourcesFolder {"res"};
std::filesystem::path currentWorldFolder;
std::optional<std::filesystem::path> scriptFolder;
std::vector<ContentPack>* contentPacks = nullptr;
};

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