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: on:
push: push:
@ -20,24 +20,33 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
with: with:
submodules: 'true' submodules: 'true'
- name: install dependencies - name: Install dependencies
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y build-essential libglfw3-dev libglfw3 libglew-dev \ 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 # 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/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 sudo ln -s /usr/include/luajit-2.1 /usr/include/lua
# install EnTT # install EnTT
git clone https://github.com/skypjack/entt.git git clone https://github.com/skypjack/entt.git
cd entt/build cd entt/build
cmake -DCMAKE_BUILD_TYPE=Release .. cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo ..
sudo make install sudo make install
cd ../.. cd ../..
- name: configure - name: Configure
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DVOXELENGINE_BUILD_APPDIR=1 run: cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo -DVOXELENGINE_BUILD_APPDIR=1 -DVOXELENGINE_BUILD_TESTS=ON
- name: build - name: Build
run: cmake --build build -t install 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 - name: Build AppImage
uses: AppImageCrafters/build-appimage-action@fe2205a4d6056be47051f7b1b3811106e9814910 uses: AppImageCrafters/build-appimage-action@fe2205a4d6056be47051f7b1b3811106e9814910
env: env:

View File

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

4
.gitignore vendored
View File

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

View File

@ -1,18 +1,24 @@
option(VOXELENGINE_BUILD_WINDOWS_VCPKG ON) cmake_minimum_required(VERSION 3.26)
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)
project(VoxelEngine) project(VoxelEngine)
option(VOXELENGINE_BUILD_APPDIR OFF) option(VOXELENGINE_BUILD_APPDIR "" OFF)
option(VOXELENGINE_BUILD_TESTS OFF) option(VOXELENGINE_BUILD_TESTS "" OFF)
set(CMAKE_CXX_STANDARD 17) 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_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) target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
if(VOXELENGINE_BUILD_APPDIR) if(VOXELENGINE_BUILD_APPDIR)
@ -24,7 +30,6 @@ if(MSVC)
set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE) set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
endif() endif()
if((CMAKE_BUILD_TYPE EQUAL "Release") OR (CMAKE_BUILD_TYPE EQUAL "RelWithDebInfo")) 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) target_compile_options(${PROJECT_NAME} PRIVATE /W4 /MT /O2)
else() else()
target_compile_options(${PROJECT_NAME} PRIVATE /W4) target_compile_options(${PROJECT_NAME} PRIVATE /W4)
@ -39,31 +44,9 @@ else()
if (CMAKE_BUILD_TYPE MATCHES "Debug") if (CMAKE_BUILD_TYPE MATCHES "Debug")
target_compile_options(${PROJECT_NAME} PRIVATE -Og) target_compile_options(${PROJECT_NAME} PRIVATE -Og)
endif() endif()
endif() if (WIN32)
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static")
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()
endif() 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() endif()
if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
@ -76,9 +59,18 @@ endif()
target_link_libraries(${PROJECT_NAME} VoxelEngineSrc ${CMAKE_DL_LIBS}) 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) if (VOXELENGINE_BUILD_TESTS)
enable_testing() enable_testing()
add_subdirectory(test) 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] >[!NOTE]
> Requirement: > 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 git clone --recursive https://github.com/MihailRis/VoxelEngine-Cpp.git
cd VoxelEngine-Cpp cd VoxelEngine-Cpp
mkdir build cmake --preset default-vs-msvc-windows
cd build cmake --build --preset default-vs-msvc-windows
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
``` ```
> [!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 ## Build using Docker
### Step 0. Install docker on your system ### 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). 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) - [UI properties and methods](scripting/ui.md)
- [Entities and components](scripting/ecs.md) - [Entities and components](scripting/ecs.md)
- [Libraries](#) - [Libraries](#)
- [app](scripting/builtins/libapp.md)
- [base64](scripting/builtins/libbase64.md) - [base64](scripting/builtins/libbase64.md)
- [bjson, json, toml](scripting/filesystem.md) - [bjson, json, toml](scripting/filesystem.md)
- [block](scripting/builtins/libblock.md) - [block](scripting/builtins/libblock.md)
- [byteutil](scripting/builtins/libbyteutil.md)
- [cameras](scripting/builtins/libcameras.md) - [cameras](scripting/builtins/libcameras.md)
- [entities](scripting/builtins/libentities.md) - [entities](scripting/builtins/libentities.md)
- [file](scripting/builtins/libfile.md) - [file](scripting/builtins/libfile.md)
@ -20,6 +22,7 @@ Subsections:
- [gfx.text3d](3d-text.md#gfxtext3d-library) - [gfx.text3d](3d-text.md#gfxtext3d-library)
- [gui](scripting/builtins/libgui.md) - [gui](scripting/builtins/libgui.md)
- [hud](scripting/builtins/libhud.md) - [hud](scripting/builtins/libhud.md)
- [input](scripting/builtins/libinput.md)
- [inventory](scripting/builtins/libinventory.md) - [inventory](scripting/builtins/libinventory.md)
- [item](scripting/builtins/libitem.md) - [item](scripting/builtins/libitem.md)
- [mat4](scripting/builtins/libmat4.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. -- Returns X: integer direction vector of the block at specified coordinates.
-- Example: no rotation: 1, 0, 0. -- Example: no rotation: 1, 0, 0.
block.get_X(x: int, y: int, z: int) -> int, int, int 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. -- Same for axis Y. Default: 0, 1, 0.
block.get_Y(x: int, y: int, z: int) -> int, int, int 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. -- Same for axis Z. Default: 0, 0, 1.
block.get_Z(x: int, y: int, z: int) -> int, int, int 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. -- Returns block rotation index based on used profile.
block.get_rotation(x: int, y: int, z: int) -> int 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. Read file into bytes array.
```lua
file.is_writeable(path: str) -> bool
```
Checks if the specified path is writable.
```python ```python
file.write(path: str, text: str) -> nil 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. 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. 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. -- Returns remaining count if could not to add fully.
inventory.add(invid: int, itemid: int, count: int) -> int 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. -- Returns block inventory ID or 0.
inventory.get_block(x: int, y: int, z: int) -> int 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. -- 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)) 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 ## TCP Connections
@ -54,6 +59,9 @@ socket:recv(
-- Closes the connection -- Closes the connection
socket:close() socket:close()
-- Returns the number of data bytes available for reading
socket:available() --> int
-- Checks that the socket exists and is not closed. -- Checks that the socket exists and is not closed.
socket:is_alive() --> bool 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) Returns the id of all base packages (non-removeable)
```python ```lua
pack.get_info(packid: str) -> { pack.get_info(packid: str) -> {
id: str, id: str,
title: str, title: str,
creator: str, creator: str,
description: str, description: str,
version: str, version: str,
icon: str, path: str,
icon: str, -- not available in headless mode
dependencies: optional strings array dependencies: optional strings array
} }
``` ```
@ -95,3 +96,15 @@ Returns information about the pack (not necessarily installed).
- `?` - optional - `?` - optional
- `~` - weak - `~` - weak
for example `!teal` 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 # *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 ```lua
player.get_pos(playerid: int) -> number, number, number 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. 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 ``` lua
player.set_spawnpoint(playerid: int, x: number, y: number, z: number) player.set_spawnpoint(playerid: int, x: number, y: number, z: number)
player.get_spawnpoint(playerid: int) -> number, number, number player.get_spawnpoint(playerid: int) -> number, number, number
@ -84,6 +103,12 @@ player.get_name(playerid: int) -> str
Player name setter and getter Player name setter and getter
```lua
player.set_selected_slot(playerid: int, slotid: int)
```
Sets the selected slot index
```lua ```lua
player.get_selected_block(playerid: int) -> x,y,z player.get_selected_block(playerid: int) -> x,y,z
``` ```

View File

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

View File

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

View File

@ -25,70 +25,4 @@ packid.binding.name="inputtype:codename"
## *input* library ## *input* library
```python See [*input* library](builtins/libinput.md)
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
```

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*. - `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. - `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. - `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 # Template attributes

View File

@ -224,3 +224,39 @@
``` ```
Пример: [пользовательские свойства пака **base**](../../res/content/base/config/user-props.toml). Пример: [пользовательские свойства пака **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) - [Свойства и методы UI элементов](scripting/ui.md)
- [Сущности и компоненты](scripting/ecs.md) - [Сущности и компоненты](scripting/ecs.md)
- [Библиотеки](#) - [Библиотеки](#)
- [app](scripting/builtins/libapp.md)
- [base64](scripting/builtins/libbase64.md) - [base64](scripting/builtins/libbase64.md)
- [bjson, json, toml](scripting/filesystem.md) - [bjson, json, toml](scripting/filesystem.md)
- [block](scripting/builtins/libblock.md) - [block](scripting/builtins/libblock.md)
- [byteutil](scripting/builtins/libbyteutil.md)
- [cameras](scripting/builtins/libcameras.md) - [cameras](scripting/builtins/libcameras.md)
- [entities](scripting/builtins/libentities.md) - [entities](scripting/builtins/libentities.md)
- [file](scripting/builtins/libfile.md) - [file](scripting/builtins/libfile.md)
@ -20,6 +22,7 @@
- [gfx.text3d](3d-text.md#библиотека-gfxtext3d) - [gfx.text3d](3d-text.md#библиотека-gfxtext3d)
- [gui](scripting/builtins/libgui.md) - [gui](scripting/builtins/libgui.md)
- [hud](scripting/builtins/libhud.md) - [hud](scripting/builtins/libhud.md)
- [input](scripting/builtins/libinput.md)
- [inventory](scripting/builtins/libinventory.md) - [inventory](scripting/builtins/libinventory.md)
- [item](scripting/builtins/libitem.md) - [item](scripting/builtins/libitem.md)
- [mat4](scripting/builtins/libmat4.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 блока на указанных координатах с учётом его вращения (три целых числа). -- Возвращает целочисленный единичный вектор X блока на указанных координатах с учётом его вращения (три целых числа).
-- Если поворот отсутствует, возвращает 1, 0, 0 -- Если поворот отсутствует, возвращает 1, 0, 0
block.get_X(x: int, y: int, z: int) -> int, int, int 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) -- То же, но для оси Y (по-умолчанию 0, 1, 0)
block.get_Y(x: int, y: int, z: int) -> int, int, int 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) -- То же, но для оси Z (по-умолчанию 0, 0, 1)
block.get_Z(x: int, y: int, z: int) -> int, int, int block.get_Z(x: int, y: int, z: int) -> int, int, int
block.get_Z(id: int, rotation: int) -> int, int, int
-- Возвращает индекс поворота блока в его профиле вращения (не превышает 7). -- Возвращает индекс поворота блока в его профиле вращения (не превышает 7).
block.get_rotation(x: int, y: int, z: int) -> int 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 ```python
file.write(путь: str, текст: str) -> nil file.write(путь: str, текст: str) -> nil
``` ```
@ -114,3 +120,27 @@ file.read_combined_object(путь: str) -> массив
``` ```
Совмещает объекты из JSON файлов разных паков. Совмещает объекты из 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 count: int
) -> 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 инвентаря блока. -- Функция возвращает id инвентаря блока.
-- Если блок не может иметь инвентарь - возвращает 0. -- Если блок не может иметь инвентарь - возвращает 0.
inventory.get_block(x: int, y: int, z: int) -> int 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)) 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-Соединения ## TCP-Соединения
@ -54,6 +59,9 @@ socket:recv(
-- Закрывает соединение -- Закрывает соединение
socket:close() socket:close()
-- Возвращает количество доступных для чтения байт данных
socket:available() --> int
-- Проверяет, что сокет существует и не закрыт. -- Проверяет, что сокет существует и не закрыт.
socket:is_alive() --> bool socket:is_alive() --> bool

View File

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

View File

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

View File

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

View File

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

View File

@ -23,70 +23,4 @@ packid.binding.name="inputtype:codename"
## Библиотека input ## Библиотека input
```python См. [библиотека *input*](builtins/libinput.md)
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
```

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

View File

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

View File

@ -9,5 +9,6 @@
"grounded": true, "grounded": true,
"model": "X", "model": "X",
"hitbox": [0.15, 0.0, 0.15, 0.7, 0.7, 0.7], "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",
"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:durability" = {}
"base:loot" = {}

View File

@ -11,4 +11,34 @@ function util.drop(ppos, itemid, count, pickup_delay)
}}) }})
end 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 return util

View File

@ -3,17 +3,32 @@ local body = entity.rigidbody
local rig = entity.skeleton local rig = entity.skeleton
local blockid = ARGS.block local blockid = ARGS.block
local blockstates = ARGS.states or 0
if SAVED_DATA.block then if SAVED_DATA.block then
blockid = SAVED_DATA.block blockid = SAVED_DATA.block
blockstates = SAVED_DATA.states or 0
else else
SAVED_DATA.block = blockid SAVED_DATA.block = blockid
SAVED_DATA.states = blockstates
end end
do -- setup visuals 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 for i,t in ipairs(textures) do
rig:set_texture("$"..tostring(i-1), "blocks:"..textures[i]) rig:set_texture("$"..tostring(i-1), "blocks:"..textures[i])
end 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 end
function on_grounded() function on_grounded()
@ -22,7 +37,7 @@ function on_grounded()
local iy = math.floor(pos[2]) local iy = math.floor(pos[2])
local iz = math.floor(pos[3]) local iz = math.floor(pos[3])
if block.is_replaceable_at(ix, iy, iz) then 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 else
local picking_item = block.get_picking_item(block.index(blockid)) local picking_item = block.get_picking_item(block.index(blockid))
local drop = entities.spawn("base:drop", pos, {base__drop={id=picking_item, count=1}}) 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) function on_block_broken(id, x, y, z, playerid)
gfx.particles.emit({x+0.5, y+0.5, z+0.5}, 64, { if gfx then
lifetime=1.0, gfx.particles.emit({x+0.5, y+0.5, z+0.5}, 64, {
spawn_interval=0.0001, lifetime=1.0,
explosion={4, 4, 4}, spawn_interval=0.0001,
texture="blocks:"..block.get_textures(id)[1], explosion={4, 4, 4},
random_sub_uv=0.1, texture="blocks:"..block.get_textures(id)[1],
size={0.1, 0.1, 0.1}, random_sub_uv=0.1,
spawn_shape="box", size={0.1, 0.1, 0.1},
spawn_spread={0.4, 0.4, 0.4} spawn_shape="box",
}) spawn_spread={0.4, 0.4, 0.4}
})
end
rules.create("do-loot-non-player", true)
end end

View File

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

View File

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

View File

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

View File

@ -40,6 +40,7 @@ local Socket = {__index={
send=function(self, ...) return network.__send(self.id, ...) end, send=function(self, ...) return network.__send(self.id, ...) end,
recv=function(self, ...) return network.__recv(self.id, ...) end, recv=function(self, ...) return network.__recv(self.id, ...) end,
close=function(self) return network.__close(self.id) end, close=function(self) return network.__close(self.id) end,
available=function(self) return network.__available(self.id) or 0 end,
is_alive=function(self) return network.__is_alive(self.id) end, is_alive=function(self) return network.__is_alive(self.id) end,
is_connected=function(self) return network.__is_connected(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, 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", "hidden", "draw-group", "picking-item", "surface-replacement", "script-name",
"ui-layout", "inventory-size", "tick-interval", "overlay-texture", "ui-layout", "inventory-size", "tick-interval", "overlay-texture",
"translucent", "fields", "particles", "icon-type", "icon", "placing-block", "translucent", "fields", "particles", "icon-type", "icon", "placing-block",
"stack-size" "stack-size", "name"
} }
for name, _ in pairs(user_props) do for name, _ in pairs(user_props) do
table.insert(names, name) table.insert(names, name)
@ -40,3 +40,24 @@ make_read_only(block.properties)
for k,v in pairs(block.properties) do for k,v in pairs(block.properties) do
make_read_only(v) make_read_only(v)
end 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
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 --------------------- ------------------- Events ---------------------
------------------------------------------------ ------------------------------------------------
@ -54,7 +135,12 @@ function events.emit(event, ...)
return nil return nil
end end
for _, func in ipairs(handlers) do 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 end
return result return result
end end
@ -85,7 +171,7 @@ function Document.new(docname)
end end
local _RadioGroup = {} local _RadioGroup = {}
function _RadioGroup.set(self, key) function _RadioGroup:set(key)
if type(self) ~= 'table' then if type(self) ~= 'table' then
error("called as non-OOP via '.', use radiogroup:set") error("called as non-OOP via '.', use radiogroup:set")
end end
@ -98,7 +184,7 @@ function _RadioGroup.set(self, key)
self.callback(key) self.callback(key)
end end
end end
function _RadioGroup.__call(self, elements, onset, default) function _RadioGroup:__call(elements, onset, default)
local group = setmetatable({ local group = setmetatable({
elements=elements, elements=elements,
callback=onset, callback=onset,
@ -114,20 +200,8 @@ _GUI_ROOT = Document.new("core:root")
_MENU = _GUI_ROOT.menu _MENU = _GUI_ROOT.menu
menu = _MENU menu = _MENU
local __post_runnables = {} local gui_util = require "core:gui_util"
__vc_page_loader = gui_util.load_page
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
--- Console library extension --- --- Console library extension ---
console.cheats = {} console.cheats = {}
@ -272,7 +346,6 @@ function __vc_on_hud_open()
_rules.create("allow-content-access", hud._is_content_access(), function(value) _rules.create("allow-content-access", hud._is_content_access(), function(value)
hud._set_content_access(value) hud._set_content_access(value)
input.set_enabled("player.pick", value)
end) end)
_rules.create("allow-flight", true, function(value) _rules.create("allow-flight", true, function(value)
input.set_enabled("player.flight", value) input.set_enabled("player.flight", value)
@ -328,6 +401,86 @@ function __vc_on_world_quit()
_rules.clear() _rules.clear()
end 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 = {}
assets.load_texture = core.__load_texture assets.load_texture = core.__load_texture

View File

@ -34,11 +34,11 @@ end
function timeit(iters, func, ...) function timeit(iters, func, ...)
local tm = time.uptime() local tm = os.clock()
for i=1,iters do for i=1,iters do
func(...) func(...)
end end
print("[time mcs]", (time.uptime()-tm) * 1000000) print("[time mcs]", (os.clock()-tm) * 1000000)
end end
---------------------------------------------- ----------------------------------------------
@ -361,3 +361,20 @@ function __vc_warning(msg, detail, n)
"core:warning", msg, detail, debug.get_traceback(1 + (n or 0))) "core:warning", msg, detail, debug.get_traceback(1 + (n or 0)))
end end
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.Apply=Применить
menu.Audio=Звук menu.Audio=Звук
menu.Back to Main Menu=Вернуться в Меню menu.Back to Main Menu=Вернуться в Меню
menu.Scripts=Сценарии
menu.Content Error=Ошибка Контента menu.Content Error=Ошибка Контента
menu.Content=Контент menu.Content=Контент
menu.Continue=Продолжить 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 HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/*.hpp)
file(GLOB_RECURSE SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp) 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}) add_library(${PROJECT_NAME} STATIC ${SOURCES} ${HEADERS})
option(VOXELENGINE_BUILD_WINDOWS_VCPKG ON)
find_package(OpenGL REQUIRED) find_package(OpenGL REQUIRED)
find_package(GLEW 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(ZLIB REQUIRED)
find_package(PNG REQUIRED) find_package(PNG REQUIRED)
find_package(CURL REQUIRED) find_package(CURL REQUIRED)
@ -20,19 +24,23 @@ if (NOT APPLE)
find_package(EnTT REQUIRED) find_package(EnTT REQUIRED)
endif() endif()
if (WIN32) set(LIBS "")
if(VOXELENGINE_BUILD_WINDOWS_VCPKG)
set(LUA_LIBRARIES "${CMAKE_CURRENT_SOURCE_DIR}/../vcpkg/packages/luajit_x64-windows/lib/lua51.lib") if (CMAKE_SYSTEM_NAME STREQUAL "Windows")
set(LUA_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../vcpkg/packages/luajit_x64-windows/include/luajit") # Use directly linking to lib instead PkgConfig (because pkg-config dont install on windows as default)
find_package(glfw3 REQUIRED) # TODO: Do it with findLua.
find_package(glm REQUIRED) if (MSVC)
find_package(vorbis REQUIRED) set(LUA_INCLUDE_DIR "$ENV{VCPKG_ROOT}/packages/luajit_${VCPKG_TARGET_TRIPLET}/include/luajit")
set(VORBISLIB Vorbis::vorbis Vorbis::vorbisfile) find_package(Lua REQUIRED)
else() else()
find_package(Lua REQUIRED) # Used for mingw-clang cross compiling from msys2
set(VORBISLIB vorbis vorbisfile) # not tested set(LIBS ${LIBS} luajit-5.1)
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/libs/glfw)
endif() endif()
find_package(glfw3 REQUIRED)
find_package(glm REQUIRED)
find_package(vorbis REQUIRED)
set(VORBISLIB Vorbis::vorbis Vorbis::vorbisfile)
elseif(APPLE) elseif(APPLE)
find_package(PkgConfig) find_package(PkgConfig)
pkg_check_modules(LUAJIT REQUIRED luajit) pkg_check_modules(LUAJIT REQUIRED luajit)
@ -53,8 +61,6 @@ else()
set(VORBISLIB ${VORBIS_LDFLAGS}) set(VORBISLIB ${VORBIS_LDFLAGS})
endif() endif()
set(LIBS "")
if(UNIX) if(UNIX)
find_package(glfw3 3.3 REQUIRED) find_package(glfw3 3.3 REQUIRED)
find_package(Threads REQUIRED) find_package(Threads REQUIRED)

View File

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

View File

@ -6,6 +6,10 @@
#include "util/data_io.hpp" #include "util/data_io.hpp"
ByteBuilder::ByteBuilder(size_t size) {
buffer.reserve(size);
}
void ByteBuilder::put(ubyte b) { void ByteBuilder::put(ubyte b) {
buffer.push_back(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(); size_t size = buffer.size();
buffer.resize(buffer.size() + sizeof(int16_t)); 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)); 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(); size_t size = buffer.size();
buffer.resize(buffer.size() + sizeof(int32_t)); 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)); 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(); size_t size = buffer.size();
buffer.resize(buffer.size() + sizeof(int64_t)); 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)); 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; int32_t i32_val;
std::memcpy(&i32_val, &val, sizeof(int32_t)); 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; int64_t i64_val;
std::memcpy(&i64_val, &val, sizeof(int64_t)); std::memcpy(&i64_val, &val, sizeof(int64_t));
putInt64(i64_val); putInt64(i64_val, bigEndian);
} }
void ByteBuilder::set(size_t position, ubyte val) { 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(); 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) { void ByteReader::checkMagic(const char* data, size_t size) {
if (pos + size >= this->size) { if (pos + size >= this->size) {
throw std::runtime_error("invalid magic number"); throw std::runtime_error("invalid magic number");
@ -129,45 +137,51 @@ ubyte ByteReader::peek() {
return data[pos]; return data[pos];
} }
int16_t ByteReader::getInt16() { int16_t ByteReader::getInt16(bool bigEndian) {
if (pos + sizeof(int16_t) > size) { if (pos + sizeof(int16_t) > size) {
throw std::runtime_error("buffer underflow"); throw std::runtime_error("buffer underflow");
} }
int16_t value; int16_t value;
std::memcpy(&value, data + pos, sizeof(int16_t)); std::memcpy(&value, data + pos, sizeof(int16_t));
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) { if (pos + sizeof(int32_t) > size) {
throw std::runtime_error("buffer underflow"); throw std::runtime_error("buffer underflow");
} }
int32_t value; int32_t value;
std::memcpy(&value, data + pos, sizeof(int32_t)); std::memcpy(&value, data + pos, sizeof(int32_t));
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) { if (pos + sizeof(int64_t) > size) {
throw std::runtime_error("buffer underflow"); throw std::runtime_error("buffer underflow");
} }
int64_t value; int64_t value;
std::memcpy(&value, data + pos, sizeof(int64_t)); std::memcpy(&value, data + pos, sizeof(int64_t));
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(); int32_t i32_val = getInt32();
if (bigEndian) {
i32_val = dataio::be2h(i32_val);
}
float val; float val;
std::memcpy(&val, &i32_val, sizeof(float)); std::memcpy(&val, &i32_val, sizeof(float));
return val; return val;
} }
double ByteReader::getFloat64() { double ByteReader::getFloat64(bool bigEndian) {
int64_t i64_val = getInt64(); int64_t i64_val = getInt64();
if (bigEndian) {
i64_val = dataio::be2h(i64_val);
}
double val; double val;
std::memcpy(&val, &i64_val, sizeof(double)); std::memcpy(&val, &i64_val, sizeof(double));
return val; return val;

View File

@ -8,20 +8,23 @@
class ByteBuilder { class ByteBuilder {
std::vector<ubyte> buffer; std::vector<ubyte> buffer;
public: public:
ByteBuilder() = default;
ByteBuilder(size_t size);
/// @brief Write one byte (8 bit unsigned integer) /// @brief Write one byte (8 bit unsigned integer)
void put(ubyte b); void put(ubyte b);
/// @brief Write c-string (bytes array terminated with '\00') /// @brief Write c-string (bytes array terminated with '\00')
void putCStr(const char* str); void putCStr(const char* str);
/// @brief Write signed 16 bit little-endian integer /// @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 /// @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 /// @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 /// @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 /// @brief Write 64 bit floating-point number
void putFloat64(double val); void putFloat64(double val, bool bigEndian = false);
/// @brief Write string (uint32 length + bytes) /// @brief Write string (uint32 length + bytes)
void put(const std::string& s); void put(const std::string& s);
@ -50,6 +53,7 @@ class ByteReader {
public: public:
ByteReader(const ubyte* data, size_t size); ByteReader(const ubyte* data, size_t size);
ByteReader(const ubyte* data); ByteReader(const ubyte* data);
ByteReader(const std::vector<ubyte>& data);
void checkMagic(const char* data, size_t size); void checkMagic(const char* data, size_t size);
/// @brief Get N bytes /// @brief Get N bytes
@ -59,15 +63,15 @@ public:
/// @brief Read one byte (unsigned 8 bit integer) without pointer move /// @brief Read one byte (unsigned 8 bit integer) without pointer move
ubyte peek(); ubyte peek();
/// @brief Read signed 16 bit little-endian integer /// @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 /// @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 /// @brief Read signed 64 bit little-endian integer
int64_t getInt64(); int64_t getInt64(bool bigEndian = false);
/// @brief Read 32 bit floating-point number /// @brief Read 32 bit floating-point number
float getFloat32(); float getFloat32(bool bigEndian = false);
/// @brief Read 64 bit floating-point number /// @brief Read 64 bit floating-point number
double getFloat64(); double getFloat64(bool bigEndian = false);
/// @brief Read C-String /// @brief Read C-String
const char* getCString(); const char* getCString();
/// @brief Read string with unsigned 32 bit number before (length) /// @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"); throw std::runtime_error("#RRGGBB or #RRGGBBAA required");
} }
int a = 255; int a = 255;
int r = (hexchar2int(text[1]) << 4) | hexchar2int(text[2]); int r = (std::max(0, hexchar2int(text[1])) << 4) | hexchar2int(text[2]);
int g = (hexchar2int(text[3]) << 4) | hexchar2int(text[4]); int g = (std::max(0, hexchar2int(text[3])) << 4) | hexchar2int(text[4]);
int b = (hexchar2int(text[5]) << 4) | hexchar2int(text[6]); int b = (std::max(0, hexchar2int(text[5])) << 4) | hexchar2int(text[6]);
if (text.length() == 9) { 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); return glm::vec4(r / 255.f, g / 255.f, b / 255.f, a / 255.f);
} else { } 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 = 8;
inline constexpr uint VOXEL_USER_BITS_OFFSET = sizeof(blockstate_t)*8-VOXEL_USER_BITS; 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) /// @brief chunk volume (count of voxels per Chunk)
inline constexpr int CHUNK_VOL = (CHUNK_W * CHUNK_H * CHUNK_D); 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) { for (Block* def : blockDefsIndices) {
def->rt.pickingItem = content->items.require(def->pickingItem).rt.id; def->rt.pickingItem = content->items.require(def->pickingItem).rt.id;
def->rt.surfaceReplacement = content->blocks.require(def->surfaceReplacement).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) { for (ItemDef* def : itemDefsIndices) {
def->rt.placingBlock = content->blocks.require(def->placingBlock).rt.id; 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()) { for (auto& [name, def] : content->generators.getDefs()) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,9 +11,9 @@
#include "voxels/Block.hpp" #include "voxels/Block.hpp"
// All in-game definitions (blocks, items, etc..) // 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.replaceable = true;
block.drawGroup = 1; block.drawGroup = 1;
block.lightPassing = true; block.lightPassing = true;
@ -24,11 +24,11 @@ void corecontent::setup(EnginePaths* paths, ContentBuilder* builder) {
block.pickingItem = CORE_EMPTY; block.pickingItem = CORE_EMPTY;
} }
{ {
ItemDef& item = builder->items.create(CORE_EMPTY); ItemDef& item = builder.items.create(CORE_EMPTY);
item.iconType = ItemIconType::NONE; 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)) { if (fs::is_regular_file(bindsFile)) {
Events::loadBindings( Events::loadBindings(
bindsFile.u8string(), files::read_string(bindsFile), BindType::BIND 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++) { for (uint i = 0; i < 6; i++) {
block.textureFaces[i] = "obstacle"; block.textureFaces[i] = "obstacle";
} }
block.hitboxes = {AABB()}; block.hitboxes = {AABB()};
block.breakable = false; block.breakable = false;
ItemDef& item = builder->items.create(CORE_OBSTACLE+".item"); ItemDef& item = builder.items.create(CORE_OBSTACLE+".item");
item.iconType = ItemIconType::BLOCK; item.iconType = ItemIconType::BLOCK;
item.icon = CORE_OBSTACLE; item.icon = CORE_OBSTACLE;
item.placingBlock = CORE_OBSTACLE; item.placingBlock = CORE_OBSTACLE;
item.caption = block.caption; 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++) { for (uint i = 0; i < 6; i++) {
block.textureFaces[i] = "struct_air"; block.textureFaces[i] = "struct_air";
} }
@ -58,7 +58,7 @@ void corecontent::setup(EnginePaths* paths, ContentBuilder* builder) {
block.lightPassing = true; block.lightPassing = true;
block.hitboxes = {AABB()}; block.hitboxes = {AABB()};
block.obstacle = false; 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.iconType = ItemIconType::BLOCK;
item.icon = CORE_STRUCT_AIR; item.icon = CORE_STRUCT_AIR;
item.placingBlock = 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_MOVE_CHEAT = "movement.cheat";
inline const std::string BIND_CAM_ZOOM = "camera.zoom"; inline const std::string BIND_CAM_ZOOM = "camera.zoom";
inline const std::string BIND_CAM_MODE = "camera.mode"; 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_ATTACK = "player.attack";
inline const std::string BIND_PLAYER_DESTROY = "player.destroy"; inline const std::string BIND_PLAYER_DESTROY = "player.destroy";
inline const std::string BIND_PLAYER_BUILD = "player.build"; 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 = inline const std::string BIND_PLAYER_FAST_INTERACTOIN =
"player.fast_interaction"; "player.fast_interaction";
inline const std::string BIND_HUD_INVENTORY = "hud.inventory"; inline const std::string BIND_HUD_INVENTORY = "hud.inventory";
@ -35,5 +32,5 @@ class EnginePaths;
class ContentBuilder; class ContentBuilder;
namespace corecontent { 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) { static inline integer_t clamp_value(integer_t value, FieldType type) {
auto typesize = sizeof_type(type) * CHAR_BIT; auto typesize = sizeof_type(type) * CHAR_BIT;
integer_t minval = -(1 << (typesize-1)); integer_t minval = -(1LL << (typesize-1));
integer_t maxval = (1 << (typesize-1))-1; integer_t maxval = (1LL << (typesize-1))-1;
return std::min(maxval, std::max(minval, value)); 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( void Logger::log(
LogLevel level, const std::string& name, const std::string& message 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; using namespace std::chrono;
std::stringstream ss; std::stringstream ss;
switch (level) { switch (level) {
case LogLevel::print:
case LogLevel::debug: case LogLevel::debug:
#ifdef NDEBUG #ifdef NDEBUG
return; return;

View File

@ -5,7 +5,7 @@
#include <sstream> #include <sstream>
namespace debug { namespace debug {
enum class LogLevel { debug, info, warning, error }; enum class LogLevel { print, debug, info, warning, error };
class Logger; class Logger;
@ -60,5 +60,10 @@ namespace debug {
LogMessage warning() { LogMessage warning() {
return LogMessage(this, LogLevel::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 supplier = std::function<T()>;
template<class T> using consumer = std::function<void(T)>; template<class T> using consumer = std::function<void(T)>;
using KeyCallback = std::function<bool()>;
// data sources // data sources
using wstringsupplier = std::function<std::wstring()>; using wstringsupplier = std::function<std::wstring()>;
using doublesupplier = std::function<double()>; using doublesupplier = std::function<double()>;

View File

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

View File

@ -1,6 +1,8 @@
#include "engine.hpp" #include "Engine.hpp"
#ifndef GLEW_STATIC
#define GLEW_STATIC #define GLEW_STATIC
#endif
#include "debug/Logger.hpp" #include "debug/Logger.hpp"
#include "assets/AssetsLoader.hpp" #include "assets/AssetsLoader.hpp"
@ -15,13 +17,10 @@
#include "content/ContentLoader.hpp" #include "content/ContentLoader.hpp"
#include "core_defs.hpp" #include "core_defs.hpp"
#include "files/files.hpp" #include "files/files.hpp"
#include "files/settings_io.hpp"
#include "frontend/locale.hpp" #include "frontend/locale.hpp"
#include "frontend/menu.hpp" #include "frontend/menu.hpp"
#include "frontend/screens/Screen.hpp" #include "frontend/screens/Screen.hpp"
#include "frontend/screens/MenuScreen.hpp"
#include "graphics/render/ModelsGenerator.hpp" #include "graphics/render/ModelsGenerator.hpp"
#include "graphics/core/Batch2D.hpp"
#include "graphics/core/DrawContext.hpp" #include "graphics/core/DrawContext.hpp"
#include "graphics/core/ImageData.hpp" #include "graphics/core/ImageData.hpp"
#include "graphics/core/Shader.hpp" #include "graphics/core/Shader.hpp"
@ -30,6 +29,7 @@
#include "logic/EngineController.hpp" #include "logic/EngineController.hpp"
#include "logic/CommandsInterpreter.hpp" #include "logic/CommandsInterpreter.hpp"
#include "logic/scripting/scripting.hpp" #include "logic/scripting/scripting.hpp"
#include "logic/scripting/scripting_hud.hpp"
#include "network/Network.hpp" #include "network/Network.hpp"
#include "util/listutil.hpp" #include "util/listutil.hpp"
#include "util/platform.hpp" #include "util/platform.hpp"
@ -37,7 +37,9 @@
#include "window/Events.hpp" #include "window/Events.hpp"
#include "window/input.hpp" #include "window/input.hpp"
#include "window/Window.hpp" #include "window/Window.hpp"
#include "settings.hpp" #include "world/Level.hpp"
#include "Mainloop.hpp"
#include "ServerMainloop.hpp"
#include <iostream> #include <iostream>
#include <assert.h> #include <assert.h>
@ -71,52 +73,69 @@ static std::unique_ptr<ImageData> load_icon(const fs::path& resdir) {
return nullptr; return nullptr;
} }
Engine::Engine(EngineSettings& settings, SettingsHandler& settingsHandler, EnginePaths* paths) Engine::Engine(CoreParameters coreParameters)
: settings(settings), settingsHandler(settingsHandler), paths(paths), : params(std::move(coreParameters)),
settings(),
settingsHandler({settings}),
interpreter(std::make_unique<cmd::CommandsInterpreter>()), interpreter(std::make_unique<cmd::CommandsInterpreter>()),
network(network::Network::create(settings.network)) network(network::Network::create(settings.network)) {
{ logger.info() << "engine version: " << ENGINE_VERSION_STRING;
paths->prepare(); 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(); loadSettings();
auto resdir = paths->getResourcesFolder(); auto resdir = paths.getResourcesFolder();
controller = std::make_unique<EngineController>(this); controller = std::make_unique<EngineController>(*this);
if (Window::initialize(&this->settings.display)){ if (!params.headless) {
throw initialize_error("could not initialize window"); 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)) { audio::initialize(settings.audio.enabled.get() && !params.headless);
icon->flipY();
Window::setIcon(icon.get());
}
loadControls();
audio::initialize(settings.audio.enabled.get());
create_channel(this, "master", settings.audio.volumeMaster); create_channel(this, "master", settings.audio.volumeMaster);
create_channel(this, "regular", settings.audio.volumeRegular); create_channel(this, "regular", settings.audio.volumeRegular);
create_channel(this, "music", settings.audio.volumeMusic); create_channel(this, "music", settings.audio.volumeMusic);
create_channel(this, "ambient", settings.audio.volumeAmbient); create_channel(this, "ambient", settings.audio.volumeAmbient);
create_channel(this, "ui", settings.audio.volumeUI); create_channel(this, "ui", settings.audio.volumeUI);
gui = std::make_unique<gui::GUI>(); bool langNotSet = settings.ui.language.get() == "auto";
if (settings.ui.language.get() == "auto") { if (langNotSet) {
settings.ui.language.set(langs::locale_by_envlocale( settings.ui.language.set(langs::locale_by_envlocale(
platform::detect_locale(), platform::detect_locale(),
paths->getResourcesFolder() paths.getResourcesFolder()
)); ));
} }
if (ENGINE_DEBUG_BUILD) { scripting::initialize(this);
menus::create_version_label(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); setLanguage(lang);
}, true)); }, true));
scripting::initialize(this);
basePacks = files::read_list(resdir/fs::path("config/builtins.list")); basePacks = files::read_list(resdir/fs::path("config/builtins.list"));
} }
void Engine::loadSettings() { void Engine::loadSettings() {
fs::path settings_file = paths->getSettingsFile(); fs::path settings_file = paths.getSettingsFile();
if (fs::is_regular_file(settings_file)) { if (fs::is_regular_file(settings_file)) {
logger.info() << "loading settings"; logger.info() << "loading settings";
std::string text = files::read_string(settings_file); std::string text = files::read_string(settings_file);
@ -130,7 +149,7 @@ void Engine::loadSettings() {
} }
void Engine::loadControls() { void Engine::loadControls() {
fs::path controls_file = paths->getControlsFile(); fs::path controls_file = paths.getControlsFile();
if (fs::is_regular_file(controls_file)) { if (fs::is_regular_file(controls_file)) {
logger.info() << "loading controls"; logger.info() << "loading controls";
std::string text = files::read_string(controls_file); std::string text = files::read_string(controls_file);
@ -143,13 +162,6 @@ void Engine::onAssetsLoaded() {
gui->onAssetsLoad(assets.get()); gui->onAssetsLoad(assets.get());
} }
void Engine::updateTimers() {
frame++;
double currentTime = Window::time();
delta = currentTime - lastTime;
lastTime = currentTime;
}
void Engine::updateHotkeys() { void Engine::updateHotkeys() {
if (Events::jpressed(keycode::F2)) { if (Events::jpressed(keycode::F2)) {
saveScreenshot(); saveScreenshot();
@ -162,67 +174,58 @@ void Engine::updateHotkeys() {
void Engine::saveScreenshot() { void Engine::saveScreenshot() {
auto image = Window::takeScreenshot(); auto image = Window::takeScreenshot();
image->flipY(); image->flipY();
fs::path filename = paths->getNewScreenshotFile("png"); fs::path filename = paths.getNewScreenshotFile("png");
imageio::write(filename.string(), image.get()); imageio::write(filename.string(), image.get());
logger.info() << "saved screenshot as " << filename.u8string(); logger.info() << "saved screenshot as " << filename.u8string();
} }
void Engine::mainloop() { void Engine::run() {
logger.info() << "starting menu screen"; if (params.headless) {
setScreen(std::make_shared<MenuScreen>(this)); ServerMainloop(*this).run();
} else {
Batch2D batch(1024); Mainloop(*this).run();
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::renderFrame(Batch2D& batch) { void Engine::postUpdate() {
screen->draw(delta); 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); Viewport viewport(Window::width, Window::height);
DrawContext ctx(nullptr, viewport, &batch); DrawContext ctx(nullptr, viewport, nullptr);
gui->draw(ctx, *assets); 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() { void Engine::saveSettings() {
logger.info() << "saving settings"; logger.info() << "saving settings";
files::write_string(paths->getSettingsFile(), toml::stringify(settingsHandler)); files::write_string(paths.getSettingsFile(), toml::stringify(settingsHandler));
logger.info() << "saving bindings"; if (!params.headless) {
files::write_string(paths->getControlsFile(), Events::writeBindings()); logger.info() << "saving bindings";
files::write_string(paths.getControlsFile(), Events::writeBindings());
}
} }
Engine::~Engine() { Engine::~Engine() {
@ -235,13 +238,19 @@ Engine::~Engine() {
content.reset(); content.reset();
assets.reset(); assets.reset();
interpreter.reset(); interpreter.reset();
gui.reset(); if (gui) {
logger.info() << "gui finished"; gui.reset();
logger.info() << "gui finished";
}
audio::close(); audio::close();
network.reset(); network.reset();
clearKeepedObjects();
scripting::close(); scripting::close();
logger.info() << "scripting finished"; logger.info() << "scripting finished";
Window::terminate(); if (!params.headless) {
Window::terminate();
logger.info() << "window closed";
}
logger.info() << "engine finished"; logger.info() << "engine finished";
} }
@ -256,13 +265,17 @@ cmd::CommandsInterpreter* Engine::getCommandsInterpreter() {
PacksManager Engine::createPacksManager(const fs::path& worldFolder) { PacksManager Engine::createPacksManager(const fs::path& worldFolder) {
PacksManager manager; PacksManager manager;
manager.setSources({ manager.setSources({
worldFolder/fs::path("content"), {"world:content", worldFolder.empty() ? worldFolder : worldFolder/fs::path("content")},
paths->getUserFilesFolder()/fs::path("content"), {"user:content", paths.getUserFilesFolder()/fs::path("content")},
paths->getResourcesFolder()/fs::path("content") {"res:content", paths.getResourcesFolder()/fs::path("content")}
}); });
return manager; return manager;
} }
void Engine::setLevelConsumer(consumer<std::unique_ptr<Level>> levelConsumer) {
this->levelConsumer = std::move(levelConsumer);
}
void Engine::loadAssets() { void Engine::loadAssets() {
logger.info() << "loading assets"; logger.info() << "loading assets";
Shader::preprocessor->setPaths(resPaths.get()); Shader::preprocessor->setPaths(resPaths.get());
@ -278,42 +291,36 @@ void Engine::loadAssets() {
auto task = loader.startTask([=](){}); auto task = loader.startTask([=](){});
task->waitForEnd(); task->waitForEnd();
} else { } else {
try { while (loader.hasNext()) {
while (loader.hasNext()) { loader.loadNext();
loader.loadNext();
}
} catch (const assetload::error& err) {
new_assets.reset();
throw;
} }
} }
assets = std::move(new_assets); assets = std::move(new_assets);
if (content) { if (content == nullptr) {
for (auto& [name, def] : content->blocks.getDefs()) { return;
if (def->model == BlockModel::custom) { }
if (def->modelName.empty()) { for (auto& [name, def] : content->blocks.getDefs()) {
assets->store( if (def->model == BlockModel::custom && def->modelName.empty()) {
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()) {
assets->store( assets->store(
std::make_unique<model::Model>( std::make_unique<model::Model>(
ModelsGenerator::generate(*def, *content, *assets) ModelsGenerator::loadCustomBlockModel(
def->customModelRaw, *assets, !def->shadeless
)
), ),
name + ".model" 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) { static void load_configs(const fs::path& root) {
@ -329,7 +336,7 @@ static void load_configs(const fs::path& root) {
void Engine::loadContent() { void Engine::loadContent() {
scripting::cleanup(); scripting::cleanup();
auto resdir = paths->getResourcesFolder(); auto resdir = paths.getResourcesFolder();
std::vector<std::string> names; std::vector<std::string> names;
for (auto& pack : contentPacks) { for (auto& pack : contentPacks) {
@ -337,12 +344,12 @@ void Engine::loadContent() {
} }
ContentBuilder contentBuilder; ContentBuilder contentBuilder;
corecontent::setup(paths, &contentBuilder); corecontent::setup(paths, contentBuilder);
paths->setContentPacks(&contentPacks); paths.setContentPacks(&contentPacks);
PacksManager manager = createPacksManager(paths->getCurrentWorldFolder()); PacksManager manager = createPacksManager(paths.getCurrentWorldFolder());
manager.scan(); manager.scan();
names = manager.assembly(names); names = manager.assemble(names);
contentPacks = manager.getAll(names); contentPacks = manager.getAll(names);
auto corePack = ContentPack::createCore(paths); auto corePack = ContentPack::createCore(paths);
@ -372,13 +379,15 @@ void Engine::loadContent() {
ContentLoader::loadScripts(*content); ContentLoader::loadScripts(*content);
langs::setup(resdir, langs::current->getId(), contentPacks); langs::setup(resdir, langs::current->getId(), contentPacks);
loadAssets(); if (!isHeadless()) {
onAssetsLoaded(); loadAssets();
onAssetsLoaded();
}
} }
void Engine::resetContent() { void Engine::resetContent() {
scripting::cleanup(); scripting::cleanup();
auto resdir = paths->getResourcesFolder(); auto resdir = paths.getResourcesFolder();
std::vector<PathsRoot> resRoots; std::vector<PathsRoot> resRoots;
{ {
auto pack = ContentPack::createCore(paths); auto pack = ContentPack::createCore(paths);
@ -395,8 +404,10 @@ void Engine::resetContent() {
content.reset(); content.reset();
langs::setup(resdir, langs::current->getId(), contentPacks); langs::setup(resdir, langs::current->getId(), contentPacks);
loadAssets(); if (!isHeadless()) {
onAssetsLoaded(); loadAssets();
onAssetsLoaded();
}
contentPacks = manager.getAll(basePacks); contentPacks = manager.getAll(basePacks);
} }
@ -405,26 +416,23 @@ void Engine::loadWorldContent(const fs::path& folder) {
contentPacks.clear(); contentPacks.clear();
auto packNames = ContentPack::worldPacksList(folder); auto packNames = ContentPack::worldPacksList(folder);
PacksManager manager; PacksManager manager;
manager.setSources({ manager.setSources(
folder/fs::path("content"), {{"world:content",
paths->getUserFilesFolder()/fs::path("content"), folder.empty() ? folder : folder / fs::path("content")},
paths->getResourcesFolder()/fs::path("content") {"user:content", paths.getUserFilesFolder() / fs::path("content")},
}); {"res:content", paths.getResourcesFolder() / fs::path("content")}}
);
manager.scan(); manager.scan();
contentPacks = manager.getAll(manager.assembly(packNames)); contentPacks = manager.getAll(manager.assemble(packNames));
paths->setCurrentWorldFolder(folder); paths.setCurrentWorldFolder(folder);
loadContent(); loadContent();
} }
void Engine::loadAllPacks() { void Engine::loadAllPacks() {
PacksManager manager = createPacksManager(paths->getCurrentWorldFolder()); PacksManager manager = createPacksManager(paths.getCurrentWorldFolder());
manager.scan(); manager.scan();
auto allnames = manager.getAllNames(); auto allnames = manager.getAllNames();
contentPacks = manager.getAll(manager.assembly(allnames)); contentPacks = manager.getAll(manager.assemble(allnames));
}
double Engine::getDelta() const {
return delta;
} }
void Engine::setScreen(std::shared_ptr<Screen> screen) { 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) { void Engine::setLanguage(std::string locale) {
langs::setup(paths->getResourcesFolder(), std::move(locale), contentPacks); langs::setup(paths.getResourcesFolder(), std::move(locale), contentPacks);
gui->getMenu()->setPageLoader(menus::create_page_loader(this)); }
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() { gui::GUI* Engine::getGUI() {
@ -469,7 +497,7 @@ std::vector<std::string>& Engine::getBasePacks() {
return basePacks; return basePacks;
} }
EnginePaths* Engine::getPaths() { EnginePaths& Engine::getPaths() {
return paths; return paths;
} }
@ -481,11 +509,6 @@ std::shared_ptr<Screen> Engine::getScreen() {
return screen; return screen;
} }
void Engine::postRunnable(const runnable& callback) {
std::lock_guard<std::recursive_mutex> lock(postRunnablesMutex);
postRunnables.push(callback);
}
SettingsHandler& Engine::getSettingsHandler() { SettingsHandler& Engine::getSettingsHandler() {
return settingsHandler; return settingsHandler;
} }
@ -493,3 +516,15 @@ SettingsHandler& Engine::getSettingsHandler() {
network::Network& Engine::getNetwork() { network::Network& Engine::getNetwork() {
return *network; 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 "delegates.hpp"
#include "typedefs.hpp" #include "typedefs.hpp"
#include "settings.hpp"
#include "assets/Assets.hpp" #include "assets/Assets.hpp"
#include "content/content_fwd.hpp" #include "content/content_fwd.hpp"
#include "content/ContentPack.hpp" #include "content/ContentPack.hpp"
#include "content/PacksManager.hpp" #include "content/PacksManager.hpp"
#include "files/engine_paths.hpp" #include "files/engine_paths.hpp"
#include "files/settings_io.hpp"
#include "util/ObjectsKeeper.hpp" #include "util/ObjectsKeeper.hpp"
#include "PostRunnables.hpp"
#include "Time.hpp"
#include <filesystem> #include <filesystem>
#include <memory> #include <memory>
#include <queue>
#include <stdexcept> #include <stdexcept>
#include <string> #include <string>
#include <vector> #include <vector>
#include <mutex>
class Level;
class Screen; class Screen;
class EnginePaths; class EnginePaths;
class ResPaths; class ResPaths;
class Batch2D;
class EngineController; class EngineController;
class SettingsHandler; class SettingsHandler;
struct EngineSettings; struct EngineSettings;
namespace fs = std::filesystem;
namespace gui { namespace gui {
class GUI; class GUI;
} }
@ -45,44 +45,52 @@ public:
initialize_error(const std::string& message) : std::runtime_error(message) {} 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 { class Engine : public util::ObjectsKeeper {
EngineSettings& settings; CoreParameters params;
SettingsHandler& settingsHandler; EngineSettings settings;
EnginePaths* paths; SettingsHandler settingsHandler;
EnginePaths paths;
std::unique_ptr<Assets> assets; std::unique_ptr<Assets> assets;
std::shared_ptr<Screen> screen; std::shared_ptr<Screen> screen;
std::vector<ContentPack> contentPacks; std::vector<ContentPack> contentPacks;
std::unique_ptr<Content> content; std::unique_ptr<Content> content;
std::unique_ptr<ResPaths> resPaths; std::unique_ptr<ResPaths> resPaths;
std::queue<runnable> postRunnables;
std::recursive_mutex postRunnablesMutex;
std::unique_ptr<EngineController> controller; std::unique_ptr<EngineController> controller;
std::unique_ptr<cmd::CommandsInterpreter> interpreter; std::unique_ptr<cmd::CommandsInterpreter> interpreter;
std::unique_ptr<network::Network> network; std::unique_ptr<network::Network> network;
std::vector<std::string> basePacks; std::vector<std::string> basePacks;
uint64_t frame = 0;
double lastTime = 0.0;
double delta = 0.0;
std::unique_ptr<gui::GUI> gui; std::unique_ptr<gui::GUI> gui;
PostRunnables postRunnables;
Time time;
consumer<std::unique_ptr<Level>> levelConsumer;
bool quitSignal = false;
void loadControls(); void loadControls();
void loadSettings(); void loadSettings();
void saveSettings(); void saveSettings();
void updateTimers();
void updateHotkeys(); void updateHotkeys();
void renderFrame(Batch2D& batch);
void processPostRunnables();
void loadAssets(); void loadAssets();
public: public:
Engine(EngineSettings& settings, SettingsHandler& settingsHandler, EnginePaths* paths); Engine(CoreParameters coreParameters);
~Engine(); ~Engine();
/// @brief Start main engine input/update/render loop. /// @brief Start the engine
/// Automatically sets MenuScreen void run();
void mainloop();
void postUpdate();
void updateFrontend();
void renderFrame();
void nextFrame();
/// @brief Called after assets loading when all engine systems are initialized /// @brief Called after assets loading when all engine systems are initialized
void onAssetsLoaded(); void onAssetsLoaded();
@ -100,6 +108,7 @@ public:
/// @brief Load all selected content-packs and reload assets /// @brief Load all selected content-packs and reload assets
void loadContent(); void loadContent();
/// @brief Reset content to base packs list
void resetContent(); void resetContent();
/// @brief Collect world content-packs and load content /// @brief Collect world content-packs and load content
@ -110,9 +119,6 @@ public:
/// @brief Collect all available content-packs from res/content /// @brief Collect all available content-packs from res/content
void loadAllPacks(); void loadAllPacks();
/// @brief Get current frame delta-time
double getDelta() const;
/// @brief Get active assets storage instance /// @brief Get active assets storage instance
Assets* getAssets(); Assets* getAssets();
@ -123,11 +129,18 @@ public:
EngineSettings& getSettings(); EngineSettings& getSettings();
/// @brief Get engine filesystem paths source /// @brief Get engine filesystem paths source
EnginePaths* getPaths(); EnginePaths& getPaths();
/// @brief Get engine resource paths controller /// @brief Get engine resource paths controller
ResPaths* getResPaths(); ResPaths* getResPaths();
void onWorldOpen(std::unique_ptr<Level> level);
void onWorldClosed();
void quit();
bool isQuitSignal() const;
/// @brief Get current Content instance /// @brief Get current Content instance
const Content* getContent() const; const Content* getContent() const;
@ -142,7 +155,9 @@ public:
std::shared_ptr<Screen> getScreen(); std::shared_ptr<Screen> getScreen();
/// @brief Enqueue function call to the end of current frame in draw thread /// @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(); void saveScreenshot();
@ -151,7 +166,15 @@ public:
PacksManager createPacksManager(const fs::path& worldFolder); PacksManager createPacksManager(const fs::path& worldFolder);
void setLevelConsumer(consumer<std::unique_ptr<Level>> levelConsumer);
SettingsHandler& getSettingsHandler(); SettingsHandler& getSettingsHandler();
network::Network& getNetwork(); 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( 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++) { for (size_t i = 0; i < list.size(); i++) {
auto& map = list[i]; auto& map = list[i];
const auto& name = map["name"].asString(); 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(); fs::path file = getResourcesFile();
if (!fs::is_regular_file(file)) { if (!fs::is_regular_file(file)) {
logger.warning() << "resources.json does not exists"; logger.warning() << "resources.json does not exists";

View File

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

View File

@ -48,6 +48,18 @@ static std::filesystem::path toCanonic(std::filesystem::path path) {
} }
void EnginePaths::prepare() { 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; auto contentFolder = userFilesFolder / CONTENT_FOLDER;
if (!fs::is_directory(contentFolder)) { if (!fs::is_directory(contentFolder)) {
fs::create_directories(contentFolder); fs::create_directories(contentFolder);
@ -120,7 +132,7 @@ std::filesystem::path EnginePaths::getSettingsFile() const {
return userFilesFolder / SETTINGS_FILE; 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; std::vector<std::filesystem::path> folders;
auto folder = getWorldsFolder(); auto folder = getWorldsFolder();
@ -157,6 +169,10 @@ void EnginePaths::setResourcesFolder(std::filesystem::path folder) {
this->resourcesFolder = std::move(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) { void EnginePaths::setCurrentWorldFolder(std::filesystem::path folder) {
this->currentWorldFolder = std::move(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( std::filesystem::path EnginePaths::resolve(
const std::string& path, bool throwErr const std::string& path, bool throwErr
) { ) const {
auto [prefix, filename] = EnginePaths::parsePath(path); auto [prefix, filename] = EnginePaths::parsePath(path);
if (prefix.empty()) { if (prefix.empty()) {
throw files_access_error("no entry point specified"); throw files_access_error("no entry point specified");
@ -199,7 +215,9 @@ std::filesystem::path EnginePaths::resolve(
if (prefix == "export") { if (prefix == "export") {
return userFilesFolder / EXPORT_FOLDER / fs::u8path(filename); return userFilesFolder / EXPORT_FOLDER / fs::u8path(filename);
} }
if (prefix == "script" && scriptFolder) {
return scriptFolder.value() / fs::u8path(filename);
}
if (contentPacks) { if (contentPacks) {
for (auto& pack : *contentPacks) { for (auto& pack : *contentPacks) {
if (pack.id == prefix) { if (pack.id == prefix) {

View File

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

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