diff --git a/.github/workflows/appimage.yml b/.github/workflows/appimage.yml index 250fd001a..54046d33c 100644 --- a/.github/workflows/appimage.yml +++ b/.github/workflows/appimage.yml @@ -1,4 +1,4 @@ -name: C/C++ AppImage +name: x86-64 AppImage on: push: @@ -20,24 +20,33 @@ jobs: - uses: actions/checkout@v2 with: submodules: 'true' - - name: install dependencies + - name: Install dependencies run: | sudo apt-get update sudo apt-get install -y build-essential libglfw3-dev libglfw3 libglew-dev \ - libglm-dev libpng-dev libopenal-dev libluajit-5.1-dev libvorbis-dev libcurl4-openssl-dev cmake squashfs-tools + libglm-dev libpng-dev libopenal-dev libluajit-5.1-dev libvorbis-dev \ + libcurl4-openssl-dev libgtest-dev cmake squashfs-tools valgrind # fix luajit paths sudo ln -s /usr/lib/x86_64-linux-gnu/libluajit-5.1.a /usr/lib/x86_64-linux-gnu/liblua5.1.a sudo ln -s /usr/include/luajit-2.1 /usr/include/lua # install EnTT git clone https://github.com/skypjack/entt.git cd entt/build - cmake -DCMAKE_BUILD_TYPE=Release .. + cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo .. sudo make install cd ../.. - - name: configure - run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DVOXELENGINE_BUILD_APPDIR=1 - - name: build + - name: Configure + run: cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo -DVOXELENGINE_BUILD_APPDIR=1 -DVOXELENGINE_BUILD_TESTS=ON + - name: Build run: cmake --build build -t install + - name: Run tests + run: ctest --test-dir build + - name: Run engine tests + timeout-minutes: 1 + run: | + chmod +x build/VoxelEngine + chmod +x AppDir/usr/bin/vctest + AppDir/usr/bin/vctest -e build/VoxelEngine -d dev/tests -u build - name: Build AppImage uses: AppImageCrafters/build-appimage-action@fe2205a4d6056be47051f7b1b3811106e9814910 env: diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index e4ddf55ab..2942ee9aa 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -39,6 +39,12 @@ jobs: - name: Run tests run: ctest --output-on-failure --test-dir build + - name: Run engine tests + timeout-minutes: 1 + run: | + chmod +x build/VoxelEngine + chmod +x AppDir/usr/bin/vctest + AppDir/usr/bin/vctest -e build/VoxelEngine -d dev/tests -u build - name: Create DMG run: | mkdir VoxelEngineDmgContent diff --git a/.github/workflows/windows-clang.yml b/.github/workflows/windows-clang.yml new file mode 100644 index 000000000..7722ce4e8 --- /dev/null +++ b/.github/workflows/windows-clang.yml @@ -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 diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index b14c32d0c..3f7efc9c3 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -1,4 +1,4 @@ -name: Windows Build +name: MSVC Build on: push: @@ -21,29 +21,33 @@ jobs: with: submodules: 'true' - - name: Set up vcpkg + - name: Bootstrap vcpkg + shell: pwsh run: | git clone https://github.com/microsoft/vcpkg.git - cd vcpkg - .\bootstrap-vcpkg.bat - .\vcpkg integrate install - cd .. + ${{ github.workspace }}/vcpkg/bootstrap-vcpkg.bat + - name: Configure and build project with CMake and vcpkg + env: + VCPKG_ROOT: ${{ github.workspace }}/vcpkg + run: | + cmake --preset default-vs-msvc-windows + cmake --build --preset default-vs-msvc-windows --config Release + - name: Run tests + run: ctest --preset default-vs-msvc-windows + - name: Run engine tests run: | - mkdir build - cd build - cmake -DCMAKE_BUILD_TYPE=Release -DVOXELENGINE_BUILD_WINDOWS_VCPKG=ON -DVOXELENGINE_BUILD_TESTS=ON .. - cmake --build . --config Release + build/vctest/Release/vctest.exe -e build/Release/VoxelEngine.exe -d dev/tests -u build + timeout-minutes: 1 - name: Package for Windows run: | mkdir packaged - cp -r build/* packaged/ - cp C:/Windows/System32/msvcp140.dll packaged/Release/msvcp140.dll - mv packaged/Release/VoxelEngine.exe packaged/Release/VoxelCore.exe + cp -r build/Release/* packaged/ + cp build/vctest/Release/vctest.exe packaged/ + cp C:/Windows/System32/msvcp140.dll packaged/msvcp140.dll + mv packaged/VoxelEngine.exe packaged/VoxelCore.exe working-directory: ${{ github.workspace }} - - name: Run tests - run: ctest --output-on-failure --test-dir build - uses: actions/upload-artifact@v4 with: name: Windows-Build - path: 'packaged/Release/*' + path: 'packaged/*' diff --git a/.gitignore b/.gitignore index a1ce6e33c..9056cce5b 100644 --- a/.gitignore +++ b/.gitignore @@ -36,10 +36,6 @@ Debug/voxel_engine AppDir appimage-build/ -# for vcpkg -/vcpkg/ -.gitmodules - # macOS folder attributes *.DS_Store diff --git a/CMakeLists.txt b/CMakeLists.txt index a56457bd2..4b5c6fc84 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,18 +1,24 @@ -option(VOXELENGINE_BUILD_WINDOWS_VCPKG ON) -if(VOXELENGINE_BUILD_WINDOWS_VCPKG AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/vcpkg/scripts/buildsystems/vcpkg.cmake") - set(CMAKE_TOOLCHAIN_FILE "${CMAKE_CURRENT_SOURCE_DIR}/vcpkg/scripts/buildsystems/vcpkg.cmake" CACHE STRING "") -endif() - -cmake_minimum_required(VERSION 3.15) +cmake_minimum_required(VERSION 3.26) project(VoxelEngine) -option(VOXELENGINE_BUILD_APPDIR OFF) -option(VOXELENGINE_BUILD_TESTS OFF) +option(VOXELENGINE_BUILD_APPDIR "" OFF) +option(VOXELENGINE_BUILD_TESTS "" OFF) set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +if (CMAKE_SYSTEM_NAME STREQUAL "Windows") + # We use two types linking: for clang build is static (vcpkg triplet x64-windows-static) + # and for msvc build is dynamic linking (vcpkg triplet x64-windows) + # By default CMAKE_MSVC_RUNTIME_LIBRARY set by MultiThreaded$<$:Debug>DLL + if (VCPKG_TARGET_TRIPLET MATCHES "static") + set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") + endif() +endif() add_subdirectory(src) -add_executable(${PROJECT_NAME} src/voxel_engine.cpp) +add_executable(${PROJECT_NAME} src/main.cpp) target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) if(VOXELENGINE_BUILD_APPDIR) @@ -24,7 +30,6 @@ if(MSVC) set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE) endif() if((CMAKE_BUILD_TYPE EQUAL "Release") OR (CMAKE_BUILD_TYPE EQUAL "RelWithDebInfo")) - set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Release>") target_compile_options(${PROJECT_NAME} PRIVATE /W4 /MT /O2) else() target_compile_options(${PROJECT_NAME} PRIVATE /W4) @@ -39,31 +44,9 @@ else() if (CMAKE_BUILD_TYPE MATCHES "Debug") target_compile_options(${PROJECT_NAME} PRIVATE -Og) endif() -endif() - -if(VOXELENGINE_BUILD_WINDOWS_VCPKG AND WIN32) - if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/vcpkg/.git") - find_package(Git QUIET) - if(GIT_FOUND) - message(STATUS "Adding vcpkg as a git submodule...") - execute_process(COMMAND ${GIT_EXECUTABLE} submodule add https://github.com/microsoft/vcpkg.git vcpkg WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) - else() - message(FATAL_ERROR "Git not found, cannot add vcpkg submodule.") - endif() - 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}) + if (WIN32) + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static") endif() - - foreach(CONFIG_TYPE ${CMAKE_CONFIGURATION_TYPES}) - string(TOUPPER ${CONFIG_TYPE} CONFIG_TYPE_UPPER) - add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_directory - ${CMAKE_SOURCE_DIR}/res ${CMAKE_BINARY_DIR}/${CONFIG_TYPE_UPPER}/res) - endforeach() endif() if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") @@ -76,9 +59,18 @@ endif() target_link_libraries(${PROJECT_NAME} VoxelEngineSrc ${CMAKE_DL_LIBS}) -file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/res DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) +# Deploy res to build dir +add_custom_command( + TARGET ${PROJECT_NAME} + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory_if_different + ${CMAKE_CURRENT_SOURCE_DIR}/res + $/res + ) if (VOXELENGINE_BUILD_TESTS) enable_testing() add_subdirectory(test) -endif() \ No newline at end of file +endif() + +add_subdirectory(vctest) diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 000000000..d4c6aa47f --- /dev/null +++ b/CMakePresets.json @@ -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 + } + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 418d372cb..e31e1436f 100644 --- a/README.md +++ b/README.md @@ -108,28 +108,32 @@ cmake --build . >[!NOTE] > Requirement: > -> vcpkg, CMake +> vcpkg, CMake, Git +There are two options to use vcpkg: +1. If you have Visual Studio installed, most likely the **VCPKG_ROOT** environment variable will already exist in **Developer Command Prompt for VS** +2. If you want use **vcpkg**, install **vcpkg** from git to you system: +```PowerShell +cd C:/ +git clone https://github.com/microsoft/vcpkg.git +cd vcpkg +.\bootstrap-vcpkg.bat +``` +After installing **vcpkg**, setup env variable **VCPKG_ROOT** and add it to **PATH**: +```PowerShell +$env:VCPKG_ROOT = "C:\path\to\vcpkg" +$env:PATH = "$env:VCPKG_ROOT;$env:PATH" +``` +>[!TIP] +>For troubleshooting you can read full [documentation](https://learn.microsoft.com/ru-ru/vcpkg/get_started/get-started?pivots=shell-powershell) for **vcpkg** -```sh +After installing **vcpkg** you can build project: +```PowerShell git clone --recursive https://github.com/MihailRis/VoxelEngine-Cpp.git cd VoxelEngine-Cpp -mkdir build -cd build -cmake -DCMAKE_BUILD_TYPE=Release -DVOXELENGINE_BUILD_WINDOWS_VCPKG=ON .. -del CMakeCache.txt -rmdir /s /q CMakeFiles -cmake -DCMAKE_BUILD_TYPE=Release -DVOXELENGINE_BUILD_WINDOWS_VCPKG=ON .. -cmake --build . --config Release +cmake --preset default-vs-msvc-windows +cmake --build --preset default-vs-msvc-windows ``` -> [!TIP] -> You can use ```rm CMakeCache.txt``` and ```rm -rf CMakeFiles``` while using Git Bash - -> [!WARNING] -> If you have issues during the vcpkg integration, try navigate to ```vcpkg\downloads``` -> and extract PowerShell-[version]-win-x86 to ```vcpkg\downloads\tools``` as powershell-core-[version]-windows. -> Then rerun ```cmake -DCMAKE_BUILD_TYPE=Release -DVOXELENGINE_BUILD_WINDOWS_VCPKG=ON ..``` - ## Build using Docker ### Step 0. Install docker on your system diff --git a/dev/tests/base_entities.lua b/dev/tests/base_entities.lua new file mode 100644 index 000000000..7b0e2e776 --- /dev/null +++ b/dev/tests/base_entities.lua @@ -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) diff --git a/dev/tests/chunks.lua b/dev/tests/chunks.lua new file mode 100644 index 000000000..fcbaeb1ff --- /dev/null +++ b/dev/tests/chunks.lua @@ -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") diff --git a/dev/tests/filesystem.lua b/dev/tests/filesystem.lua new file mode 100644 index 000000000..a9ff58ce0 --- /dev/null +++ b/dev/tests/filesystem.lua @@ -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:")) diff --git a/dev/tests/world.lua b/dev/tests/world.lua new file mode 100644 index 000000000..db1ffca53 --- /dev/null +++ b/dev/tests/world.lua @@ -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") diff --git a/dev/valgrind.suppress b/dev/valgrind.suppress new file mode 100644 index 000000000..7e12d4735 --- /dev/null +++ b/dev/valgrind.suppress @@ -0,0 +1,46 @@ +# Valgrind suppression file for VoxelCore +{ + + Memcheck:Cond + obj:/usr/lib/x86_64-linux-gnu/libluajit-5.1.so.2.1.0 + fun:lua_getfield +} +{ + + Memcheck:Cond + obj:/usr/lib/x86_64-linux-gnu/libluajit-5.1.so.2.1.0 + fun:lua_setfield +} +{ + + 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 +} +{ + + Memcheck:Cond + obj:/usr/lib/x86_64-linux-gnu/libluajit-5.1.so.2.1.0 + fun:lua_pushstring +} +{ + + 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 +} diff --git a/doc/en/block-properties.md b/doc/en/block-properties.md index 87df173b6..f5478e0b7 100644 --- a/doc/en/block-properties.md +++ b/doc/en/block-properties.md @@ -213,3 +213,39 @@ User properties must be declared in `pack:config/user-props.toml` file: ``` Example: [user properties of pack **base**](../../res/content/base/config/user-props.toml). + +## Properties introduced by the `base` pack + +### *base:durability* + +The time it takes to break a block without tools or effects, measured in seconds. + +### Loot - *base:loot* + +A list of tables with properties: + +```json +{ + "item": "pack:item", + "min": 1, + "max": 3, + "chance": 0.5 +} +``` + +- `count` defaults to 1. It does not need to be specified if `min` and `max` are provided. +- `min`, `max` - the minimum and maximum quantity of the item. +- `chance` - the probability of the item dropping. Defaults to 1.0. + +It should be noted that the `item` refers specifically to the item. That is, to specify the item of a block, you need to add `.item` after the block name. +Example: `base:dirt.item`. + +To generate loot, the function `block_loot(block_id: int)` in the `base:util` module should be used. + +## Methods + +Methods are used to manage the overwriting of properties when extending a block with other packs. + +### `property_name@append` + +Adds elements to the end of the list instead of completely overwriting it. diff --git a/doc/en/scripting.md b/doc/en/scripting.md index b00930f1f..fa45ec95f 100644 --- a/doc/en/scripting.md +++ b/doc/en/scripting.md @@ -9,9 +9,11 @@ Subsections: - [UI properties and methods](scripting/ui.md) - [Entities and components](scripting/ecs.md) - [Libraries](#) + - [app](scripting/builtins/libapp.md) - [base64](scripting/builtins/libbase64.md) - [bjson, json, toml](scripting/filesystem.md) - [block](scripting/builtins/libblock.md) + - [byteutil](scripting/builtins/libbyteutil.md) - [cameras](scripting/builtins/libcameras.md) - [entities](scripting/builtins/libentities.md) - [file](scripting/builtins/libfile.md) @@ -20,6 +22,7 @@ Subsections: - [gfx.text3d](3d-text.md#gfxtext3d-library) - [gui](scripting/builtins/libgui.md) - [hud](scripting/builtins/libhud.md) + - [input](scripting/builtins/libinput.md) - [inventory](scripting/builtins/libinventory.md) - [item](scripting/builtins/libitem.md) - [mat4](scripting/builtins/libmat4.md) diff --git a/doc/en/scripting/builtins/libapp.md b/doc/en/scripting/builtins/libapp.md new file mode 100644 index 000000000..e3fed3d4b --- /dev/null +++ b/doc/en/scripting/builtins/libapp.md @@ -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. diff --git a/doc/en/scripting/builtins/libblock.md b/doc/en/scripting/builtins/libblock.md index 5bd0536e2..61ea0edf5 100644 --- a/doc/en/scripting/builtins/libblock.md +++ b/doc/en/scripting/builtins/libblock.md @@ -67,12 +67,15 @@ Following three functions return direction vectors based on block rotation. -- Returns X: integer direction vector of the block at specified coordinates. -- Example: no rotation: 1, 0, 0. block.get_X(x: int, y: int, z: int) -> int, int, int +block.get_X(id: int, rotation: int) -> int, int, int -- Same for axis Y. Default: 0, 1, 0. block.get_Y(x: int, y: int, z: int) -> int, int, int +block.get_Y(id: int, rotation: int) -> int, int, int -- Same for axis Z. Default: 0, 0, 1. block.get_Z(x: int, y: int, z: int) -> int, int, int +block.get_Z(id: int, rotation: int) -> int, int, int -- Returns block rotation index based on used profile. block.get_rotation(x: int, y: int, z: int) -> int diff --git a/doc/en/scripting/builtins/libbyteutil.md b/doc/en/scripting/builtins/libbyteutil.md new file mode 100644 index 000000000..c0cf72d07 --- /dev/null +++ b/doc/en/scripting/builtins/libbyteutil.md @@ -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 +``` diff --git a/doc/en/scripting/builtins/libfile.md b/doc/en/scripting/builtins/libfile.md index fe02bed18..9f1754d7d 100644 --- a/doc/en/scripting/builtins/libfile.md +++ b/doc/en/scripting/builtins/libfile.md @@ -25,6 +25,12 @@ file.read_bytes(path: str) -> array of integers Read file into bytes array. +```lua +file.is_writeable(path: str) -> bool +``` + +Checks if the specified path is writable. + ```python file.write(path: str, text: str) -> nil ``` @@ -114,3 +120,27 @@ file.read_combined_object(path: str) -> array ``` Combines objects from JSON files of different packs. + +```lua +file.name(path: str) --> str +``` + +Extracts the file name from the path. Example: `world:data/base/config.toml` -> `config.toml`. + +``lua +file.stem(path: str) --> str +``` + +Extracts the file name from the path, removing the extension. Example: `world:data/base/config.toml` -> `config`. + +```lua +file.ext(path: str) --> str +``` + +Extracts the extension from the path. Example: `world:data/base/config.toml` -> `toml`. + +```lua +file.prefix(path: str) --> str +``` + +Extracts the entry point (prefix) from the path. Example: `world:data/base/config.toml` -> `world`. diff --git a/doc/en/scripting/builtins/libgui.md b/doc/en/scripting/builtins/libgui.md index 3132bce3c..64b5005be 100644 --- a/doc/en/scripting/builtins/libgui.md +++ b/doc/en/scripting/builtins/libgui.md @@ -61,3 +61,34 @@ gui.escape_markup( ``` Escapes markup in text. + +```lua +gui.confirm( + -- message (does not translate automatically, use gui.str(...)) + message: str, + -- function called upon confirmation + on_confirm: function() -> nil, + -- function called upon denial/cancellation + [optional] on_deny: function() -> nil, + -- text for the confirmation button (default: "Yes") + -- use an empty string for the default value if you want to specify no_text. + [optional] yes_text: str, + -- text for the denial button (default: "No") + [optional] no_text: str, +) +``` + +Requests confirmation from the user for an action. **Does not** stop code execution. + +```lua +gui.load_document( + -- Path to the xml file of the page. Example: `core:layouts/pages/main.xml` + path: str, + -- Name (id) of the document. Example: `core:pages/main` + name: str + -- Table of parameters passed to the on_open event + args: table +) --> str +``` + +Loads a UI document with its script, returns the name of the document if successfully loaded. diff --git a/doc/en/scripting/builtins/libinput.md b/doc/en/scripting/builtins/libinput.md new file mode 100644 index 000000000..02f2a8d08 --- /dev/null +++ b/doc/en/scripting/builtins/libinput.md @@ -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 +``` + diff --git a/doc/en/scripting/builtins/libinventory.md b/doc/en/scripting/builtins/libinventory.md index ccb7be20d..a3f3632fb 100644 --- a/doc/en/scripting/builtins/libinventory.md +++ b/doc/en/scripting/builtins/libinventory.md @@ -32,6 +32,21 @@ inventory.size(invid: int) -> int -- Returns remaining count if could not to add fully. inventory.add(invid: int, itemid: int, count: int) -> int +-- Returns the index of the first matching slot in the given range. +-- If no matching slot was found, returns nil +inventory.find_by_item( + -- inventory id + invid: int, + -- item id + itemid: int, + -- [optional] index of the slot range start (from 0) + range_begin: int, + -- [optional] index of the slot range end (from 0) + range_end: int, + -- [optional] minimum item count in the slot + min_count: int = 1 +) -> int + -- Returns block inventory ID or 0. inventory.get_block(x: int, y: int, z: int) -> int diff --git a/doc/en/scripting/builtins/libnetwork.md b/doc/en/scripting/builtins/libnetwork.md index 48caefbdc..cb24e7bf7 100644 --- a/doc/en/scripting/builtins/libnetwork.md +++ b/doc/en/scripting/builtins/libnetwork.md @@ -16,6 +16,11 @@ end) -- A variant for binary files, with a byte array instead of a string in the response. network.get_binary(url: str, callback: function(table|ByteArray)) + +-- Performs a POST request to the specified URL. +-- Currently, only `Content-Type: application/json` is supported +-- After receiving the response, passes the text to the callback function. +network.post(url: str, data: table, callback: function(str)) ``` ## TCP Connections @@ -54,6 +59,9 @@ socket:recv( -- Closes the connection socket:close() +-- Returns the number of data bytes available for reading +socket:available() --> int + -- Checks that the socket exists and is not closed. socket:is_alive() --> bool diff --git a/doc/en/scripting/builtins/libpack.md b/doc/en/scripting/builtins/libpack.md index 0fbc4b158..35b11088c 100644 --- a/doc/en/scripting/builtins/libpack.md +++ b/doc/en/scripting/builtins/libpack.md @@ -76,14 +76,15 @@ pack.get_base_packs() -> strings array Returns the id of all base packages (non-removeable) -```python +```lua pack.get_info(packid: str) -> { id: str, title: str, creator: str, description: str, version: str, - icon: str, + path: str, + icon: str, -- not available in headless mode dependencies: optional strings array } ``` @@ -95,3 +96,15 @@ Returns information about the pack (not necessarily installed). - `?` - optional - `~` - weak for example `!teal` + +To obtain information about multiple packs, use table of ids to avoid re-scanning:one + +```lua +pack.get_info(packids: table) -> {id={...}, id2={...}, ...} +``` + +```lua +pack.assemble(packis: table) -> table +``` + +Checks the configuration for correctness and adds dependencies, returning the complete configuration. diff --git a/doc/en/scripting/builtins/libplayer.md b/doc/en/scripting/builtins/libplayer.md index b43bf8607..041bf4423 100644 --- a/doc/en/scripting/builtins/libplayer.md +++ b/doc/en/scripting/builtins/libplayer.md @@ -1,5 +1,17 @@ # *player* library +```lua +player.create(name: str) -> int +``` + +Creates a player and returns id. + +```lua +player.delete(id: int) +``` + +Deletes a player by id. + ```lua player.get_pos(playerid: int) -> number, number, number ``` @@ -70,6 +82,13 @@ player.set_instant_destruction(playerid: int, bool) Getter and setter for instant destruction of blocks when the `player.destroy` binding is activated. +```lua +player.is_loading_chunks(playerid: int) -> bool +player.set_loading_chunks(playerid: int, bool) +``` + +Getter and setter of the property that determines whether the player is loading chunks. + ``` lua player.set_spawnpoint(playerid: int, x: number, y: number, z: number) player.get_spawnpoint(playerid: int) -> number, number, number @@ -84,6 +103,12 @@ player.get_name(playerid: int) -> str Player name setter and getter +```lua +player.set_selected_slot(playerid: int, slotid: int) +``` + +Sets the selected slot index + ```lua player.get_selected_block(playerid: int) -> x,y,z ``` diff --git a/doc/en/scripting/builtins/libworld.md b/doc/en/scripting/builtins/libworld.md index 5bf01abb0..fc18773e5 100644 --- a/doc/en/scripting/builtins/libworld.md +++ b/doc/en/scripting/builtins/libworld.md @@ -36,14 +36,15 @@ world.get_seed() -> int -- Returns generator name. world.get_generator() -> str --- Proves that this is the current time during the day --- from 0.333(8 am) to 0.833(8 pm). -world.is_day() -> boolean +-- Checks the existence of a world by name. +world.exists(name: str) -> bool + +-- Checks if the current time is daytime. From 0.333(8am) to 0.833(8pm). +world.is_day() -> bool --- Checks that it is the current time at night --- from 0.833(8 pm) to 0.333(8 am). +-- Checks if the current time is nighttime. From 0.833(8pm) to 0.333(8am). world.is_night() -> bool --- Checks the existence of a world by name. -world.exists() -> bool +-- Returns the total number of chunks loaded into memory +world.count_chunks() -> int ``` diff --git a/doc/en/scripting/ui.md b/doc/en/scripting/ui.md index c990a1977..5013b4487 100644 --- a/doc/en/scripting/ui.md +++ b/doc/en/scripting/ui.md @@ -48,6 +48,7 @@ Properties that apply to all elements: | tooltip | string | yes | yes | tooltip text | | tooltipDelay | float | yes | yes | tooltip delay | | contentOffset | vec2 | yes | *no* | element content offset | +| cursor | string | yes | yes | cursor displayed on hover | Common element methods: diff --git a/doc/en/scripting/user-input.md b/doc/en/scripting/user-input.md index 402f9e67b..d8a6e4e8e 100644 --- a/doc/en/scripting/user-input.md +++ b/doc/en/scripting/user-input.md @@ -25,70 +25,4 @@ packid.binding.name="inputtype:codename" ## *input* library -```python -input.keycode(keyname: str) -> int -``` - -Returns key code or -1 if unknown - -```python -input.mousecode(mousename: str) -> int -``` - -Returns mouse button code or -1 if unknown - -```python -input.add_callback(bindname: str, callback: function) -``` - -Add binding activation callback. Example: -```lua -input.add_callback("hud.inventory", function () - print("Inventory open key pressed") -end) -``` - -```python -input.get_mouse_pos() -> {int, int} -``` - -Returns cursor screen position. - -```python -input.get_bindings() -> strings array -``` - -Returns all binding names. - -```python -input.get_binding_text(bindname: str) -> str -``` - -Returns text representation of button by binding name. - -```python -input.is_active(bindname: str) -> bool -``` - -Checks if the binding is active. - -```python -input.set_enabled(bindname: str, flag: bool) -``` - -Enables/disables binding until leaving the world. - -```python -input.is_pressed(code: str) -> bool -``` - -Checks input activity using a code consisting of: -- input type: *key* or *mouse* -- input code: [key name](#key names) or mouse button name (left, middle, right) - -Example: -```lua -if input.is_pressed("key:enter") then - ... -end -``` +See [*input* library](builtins/libinput.md) diff --git a/doc/en/xml-ui-layouts.md b/doc/en/xml-ui-layouts.md index e70086825..3f0bff071 100644 --- a/doc/en/xml-ui-layouts.md +++ b/doc/en/xml-ui-layouts.md @@ -44,6 +44,7 @@ Examples: - `gravity` - automatic positioning of the element in the container. (Does not work in automatic containers like panel). Values: *top-left, top-center, top-right, center-left, center-center, center-right, bottom-left, bottom-center, bottom-right*. - `z-index` - determines the order of elements, with a larger value it will overlap elements with a smaller one. - `interactive` - if false, hovering over the element and all sub-elements will be ignored. +- `cursor` - the cursor displayed when hovering over the element (arrow/text/pointer/crosshair/ew-resize/ns-resize/...). # Template attributes diff --git a/doc/ru/block-properties.md b/doc/ru/block-properties.md index 2f958e889..3b75759eb 100644 --- a/doc/ru/block-properties.md +++ b/doc/ru/block-properties.md @@ -224,3 +224,39 @@ ``` Пример: [пользовательские свойства пака **base**](../../res/content/base/config/user-props.toml). + +## Свойства, вводимые паком `base` + +### Прочность - *base:durability* + +Время разрушения блока без инструментов и эффектов в секундах. + +### Лут - *base:loot* + +Список таблиц со свойствами: + +```json +{ + "item": "пак:предмет", + "min": 1, + "max": 3, + "chance": 0.5 +} +``` + +- count равен 1 по-умолчанию. Не нужно указывать если указаны `min` и `max`. +- min, max - минимальное и максимальное количество предмета. +- chance - вероятность выпадения предмета. По-умолчанию: 1.0. + +Следует учитывать, что в item указывается именно предмет. Т.е. чтобы указать предмет блока, нужно добавить `.item` после имени блока. +Пример: `base:dirt.item`. + +Для генерации лута следует использовать функцию `block_loot(block_id: int)` в модуле `base:util`. + +## Методы + +Методы используются для управлением перезаписью свойств при расширении блока другими паками. + +### `имя_свойства@append` + +Добавляет элементы в конец списка, вместо его полной перезаписи. diff --git a/doc/ru/scripting.md b/doc/ru/scripting.md index 63191ea83..e9fe3c50a 100644 --- a/doc/ru/scripting.md +++ b/doc/ru/scripting.md @@ -9,9 +9,11 @@ - [Свойства и методы UI элементов](scripting/ui.md) - [Сущности и компоненты](scripting/ecs.md) - [Библиотеки](#) + - [app](scripting/builtins/libapp.md) - [base64](scripting/builtins/libbase64.md) - [bjson, json, toml](scripting/filesystem.md) - [block](scripting/builtins/libblock.md) + - [byteutil](scripting/builtins/libbyteutil.md) - [cameras](scripting/builtins/libcameras.md) - [entities](scripting/builtins/libentities.md) - [file](scripting/builtins/libfile.md) @@ -20,6 +22,7 @@ - [gfx.text3d](3d-text.md#библиотека-gfxtext3d) - [gui](scripting/builtins/libgui.md) - [hud](scripting/builtins/libhud.md) + - [input](scripting/builtins/libinput.md) - [inventory](scripting/builtins/libinventory.md) - [item](scripting/builtins/libitem.md) - [mat4](scripting/builtins/libmat4.md) diff --git a/doc/ru/scripting/builtins/libapp.md b/doc/ru/scripting/builtins/libapp.md new file mode 100644 index 000000000..7887bcdf6 --- /dev/null +++ b/doc/ru/scripting/builtins/libapp.md @@ -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 +} +``` + +Возвращает таблицу с информацией о настройке. Бросает исключение, если настройки не существует. diff --git a/doc/ru/scripting/builtins/libblock.md b/doc/ru/scripting/builtins/libblock.md index 19a28198c..5cb149e39 100644 --- a/doc/ru/scripting/builtins/libblock.md +++ b/doc/ru/scripting/builtins/libblock.md @@ -90,12 +90,15 @@ block.raycast(start: vec3, dir: vec3, max_distance: number, [опциональ -- Возвращает целочисленный единичный вектор X блока на указанных координатах с учётом его вращения (три целых числа). -- Если поворот отсутствует, возвращает 1, 0, 0 block.get_X(x: int, y: int, z: int) -> int, int, int +block.get_X(id: int, rotation: int) -> int, int, int -- То же, но для оси Y (по-умолчанию 0, 1, 0) block.get_Y(x: int, y: int, z: int) -> int, int, int +block.get_Y(id: int, rotation: int) -> int, int, int -- То же, но для оси Z (по-умолчанию 0, 0, 1) block.get_Z(x: int, y: int, z: int) -> int, int, int +block.get_Z(id: int, rotation: int) -> int, int, int -- Возвращает индекс поворота блока в его профиле вращения (не превышает 7). block.get_rotation(x: int, y: int, z: int) -> int diff --git a/doc/ru/scripting/builtins/libbyteutil.md b/doc/ru/scripting/builtins/libbyteutil.md new file mode 100644 index 000000000..2c675594e --- /dev/null +++ b/doc/ru/scripting/builtins/libbyteutil.md @@ -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 +``` diff --git a/doc/ru/scripting/builtins/libfile.md b/doc/ru/scripting/builtins/libfile.md index a9a9c273c..45ca462c1 100644 --- a/doc/ru/scripting/builtins/libfile.md +++ b/doc/ru/scripting/builtins/libfile.md @@ -25,6 +25,12 @@ file.read_bytes(путь: str) -> array of integers Читает файл в массив байт. +```lua +file.is_writeable(путь: str) -> bool +``` + +Проверяет, доступно ли право записи по указанному пути. + ```python file.write(путь: str, текст: str) -> nil ``` @@ -114,3 +120,27 @@ file.read_combined_object(путь: str) -> массив ``` Совмещает объекты из JSON файлов разных паков. + +```lua +file.name(путь: str) --> str +``` + +Извлекает имя файла из пути. Пример: `world:data/base/config.toml` -> `config.toml`. + +```lua +file.stem(путь: str) --> str +``` + +Извлекает имя файла из пути, удаляя расширение. Пример: `world:data/base/config.toml` -> `config`. + +```lua +file.ext(путь: str) --> str +``` + +Извлекает расширение из пути. Пример: `world:data/base/config.toml` -> `toml`. + +```lua +file.prefix(путь: str) --> str +``` + +Извлекает точку входа (префикс) из пути. Пример: `world:data/base/config.toml` -> `world`. diff --git a/doc/ru/scripting/builtins/libgui.md b/doc/ru/scripting/builtins/libgui.md index 11dfde034..66e67809e 100644 --- a/doc/ru/scripting/builtins/libgui.md +++ b/doc/ru/scripting/builtins/libgui.md @@ -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 документ с его скриптом, возвращает имя документа, если успешно загружен. diff --git a/doc/ru/scripting/builtins/libinput.md b/doc/ru/scripting/builtins/libinput.md new file mode 100644 index 000000000..afe22ee62 --- /dev/null +++ b/doc/ru/scripting/builtins/libinput.md @@ -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 +``` diff --git a/doc/ru/scripting/builtins/libinventory.md b/doc/ru/scripting/builtins/libinventory.md index 7f3cf26e5..a01bd9177 100644 --- a/doc/ru/scripting/builtins/libinventory.md +++ b/doc/ru/scripting/builtins/libinventory.md @@ -38,6 +38,21 @@ inventory.add( count: int ) -> int +-- Возвращает индекс первого подходящего под критерии слота в заданном диапазоне. +-- Если подходящий слот не был найден, возвращает nil +inventory.find_by_item( + -- id инвентаря + invid: int, + -- id предмета + itemid: int, + -- [опционально] индекс начала диапазона слотов (c 0) + range_begin: int, + -- [опционально] индекс конца диапазона слотов (c 0) + range_end: int, + -- [опционально] минимальное количество предмета в слоте + min_count: int = 1 +) -> int + -- Функция возвращает id инвентаря блока. -- Если блок не может иметь инвентарь - возвращает 0. inventory.get_block(x: int, y: int, z: int) -> int diff --git a/doc/ru/scripting/builtins/libnetwork.md b/doc/ru/scripting/builtins/libnetwork.md index 7fedf2bd8..657ad7cf3 100644 --- a/doc/ru/scripting/builtins/libnetwork.md +++ b/doc/ru/scripting/builtins/libnetwork.md @@ -16,6 +16,11 @@ end) -- Вариант для двоичных файлов, с массивом байт вместо строки в ответе. network.get_binary(url: str, callback: function(table|ByteArray)) + +-- Выполняет POST запрос к указанному URL. +-- На данный момент реализована поддержка только `Content-Type: application/json` +-- После получения ответа, передаёт текст в функцию callback. +network.post(url: str, data: table, callback: function(str)) ``` ## TCP-Соединения @@ -54,6 +59,9 @@ socket:recv( -- Закрывает соединение socket:close() +-- Возвращает количество доступных для чтения байт данных +socket:available() --> int + -- Проверяет, что сокет существует и не закрыт. socket:is_alive() --> bool diff --git a/doc/ru/scripting/builtins/libpack.md b/doc/ru/scripting/builtins/libpack.md index 543ea7a35..7b3a03c12 100644 --- a/doc/ru/scripting/builtins/libpack.md +++ b/doc/ru/scripting/builtins/libpack.md @@ -63,14 +63,15 @@ pack.get_base_packs() -> массив строк Возвращает id всех базовых паков (неудаляемых) -```python +```lua pack.get_info(packid: str) -> { id: str, title: str, creator: str, description: str, version: str, - icon: str, + path: str, + icon: str, -- отсутствует в headless режиме dependencies: опциональный массив строк } ``` @@ -82,3 +83,16 @@ pack.get_info(packid: str) -> { - `?` - optional - `~` - weak например `!teal` + +Для получения информации о нескольких паках используйте таблицу id, чтобы не +производить сканирование для каждого пака: + +```lua +pack.get_info(packids: table) -> {id={...}, id2={...}, ...} +``` + +```lua +pack.assemble(packis: table) -> table +``` + +Проверяет корректность конфигурации и добавляет зависимости, возвращая полную. diff --git a/doc/ru/scripting/builtins/libplayer.md b/doc/ru/scripting/builtins/libplayer.md index 284f725a5..a96620157 100644 --- a/doc/ru/scripting/builtins/libplayer.md +++ b/doc/ru/scripting/builtins/libplayer.md @@ -1,5 +1,17 @@ # Библиотека *player* +```lua +player.create(name: str) -> int +``` + +Создаёт игрока и возвращает его id. + +```lua +player.delete(id: int) +``` + +Удаляет игрока по id. + ```lua player.get_pos(playerid: int) -> number, number, number ``` @@ -70,6 +82,13 @@ player.set_instant_destruction(playerid: int, bool) Геттер и сеттер мнгновенного разрушения блоков при активации привязки `player.destroy`. +```lua +player.is_loading_chunks(playerid: int) -> bool +player.set_loading_chunks(playerid: int, bool) +``` + +Геттер и сеттер свойства, определяющего, прогружает ли игрок чанки вокруг. + ```lua player.set_spawnpoint(playerid: int, x: number, y: number, z: number) player.get_spawnpoint(playerid: int) -> number, number, number @@ -84,6 +103,12 @@ player.get_name(playerid: int) -> str Сеттер и геттер имени игрока +```lua +player.set_selected_slot(playerid: int, slotid: int) +``` + +Устанавливает индекс выбранного слота + ```lua player.get_selected_block(playerid: int) -> x,y,z ``` diff --git a/doc/ru/scripting/builtins/libworld.md b/doc/ru/scripting/builtins/libworld.md index 9d7d635ea..0467d784c 100644 --- a/doc/ru/scripting/builtins/libworld.md +++ b/doc/ru/scripting/builtins/libworld.md @@ -36,11 +36,14 @@ world.get_seed() -> int world.get_generator() -> str -- Проверяет существование мира по имени. -world.exists() -> bool +world.exists(name: str) -> bool -- Проверяет является ли текущее время днём. От 0.333(8 утра) до 0.833(8 вечера). world.is_day() -> bool -- Проверяет является ли текущее время ночью. От 0.833(8 вечера) до 0.333(8 утра). world.is_night() -> bool + +-- Возвращает общее количество загруженных в память чанков +world.count_chunks() -> int ``` diff --git a/doc/ru/scripting/ui.md b/doc/ru/scripting/ui.md index 07d93013e..3c203d37a 100644 --- a/doc/ru/scripting/ui.md +++ b/doc/ru/scripting/ui.md @@ -48,6 +48,7 @@ document["worlds-panel"]:clear() | tooltip | string | да | да | текст всплывающей подсказки | | tooltipDelay | float | да | да | задержка всплывающей подсказки | | contentOffset | vec2 | да | *нет* | смещение содержимого | +| cursor | string | да | да | курсор, отображаемый при наведении | Общие методы элементов: diff --git a/doc/ru/scripting/user-input.md b/doc/ru/scripting/user-input.md index 686d962b2..ffcadd27c 100644 --- a/doc/ru/scripting/user-input.md +++ b/doc/ru/scripting/user-input.md @@ -23,70 +23,4 @@ packid.binding.name="inputtype:codename" ## Библиотека input -```python -input.keycode(keyname: str) -> int -``` - -Возвращает код клавиши по имени, либо -1 - -```python -input.mousecode(mousename: str) -> int -``` - -Возвращает код кнопки мыши по имени, либо -1 - -```python -input.add_callback(bindname: str, callback: function) -``` - -Назначает функцию, которая будет вызываться при активации привязки. Пример: -```lua -input.add_callback("hud.inventory", function () - print("Inventory open key pressed") -end) -``` - -```python -input.get_mouse_pos() -> {int, int} -``` - -Возвращает позицию курсора на экране. - -```python -input.get_bindings() -> массив строк -``` - -Возвращает названия всех доступных привязок. - -```python -input.get_binding_text(bindname: str) -> str -``` - -Возвращает текстовое представление кнопки по имени привязки. - -```python -input.is_active(bindname: str) -> bool -``` - -Проверяет активность привязки. - -```python -input.set_enabled(bindname: str, flag: bool) -``` - -Включает/выключает привязку до выхода из мира. - -```python -input.is_pressed(code: str) -> bool -``` - -Проверяет активность ввода по коду, состоящему из: -- типа ввода: key (клавиша) или mouse (кнопка мыши) -- код ввода: [имя клавиши](#имена-клавиш) или имя кнопки мыши (left, middle, right) - -Пример: -```lua -if input.is_pressed("key:enter") then - ... -end -``` +См. [библиотека *input*](builtins/libinput.md) diff --git a/doc/ru/xml-ui-layouts.md b/doc/ru/xml-ui-layouts.md index 1ad93c4fa..f562ee4cb 100644 --- a/doc/ru/xml-ui-layouts.md +++ b/doc/ru/xml-ui-layouts.md @@ -48,6 +48,7 @@ - `gravity` - автоматическое позиционирование элемента в контейнере. (Не работает в автоматических контейнерах, как panel). Значения: *top-left, top-center, top-right, center-left, center-center, center-right, bottom-left, bottom-center, bottom-right*. - `z-index` - определяет порядок элементов, при большем значении будет перекрывать элементы с меньшим. - `interactive` - при значении false наведение на элемент и все под-элементы будет игнорироваться. +- `cursor` - курсор, отображаемый при наведении на элемент (arrow/text/pointer/crosshair/ew-resize/ns-resize/...). # Атрибуты шаблонов diff --git a/res/content/base/blocks/bazalt.json b/res/content/base/blocks/bazalt.json index bb941a812..8c431da67 100644 --- a/res/content/base/blocks/bazalt.json +++ b/res/content/base/blocks/bazalt.json @@ -1,4 +1,5 @@ { "texture": "bazalt", - "breakable": false + "breakable": false, + "base:durability": 1e9 } diff --git a/res/content/base/blocks/grass.json b/res/content/base/blocks/grass.json index 7308d1a17..e55bc2ac3 100644 --- a/res/content/base/blocks/grass.json +++ b/res/content/base/blocks/grass.json @@ -9,5 +9,6 @@ "grounded": true, "model": "X", "hitbox": [0.15, 0.0, 0.15, 0.7, 0.7, 0.7], - "base:durability": 0.0 + "base:durability": 0.0, + "base:loot": [] } diff --git a/res/content/base/blocks/grass_block.json b/res/content/base/blocks/grass_block.json index a6087afcf..e89a774c0 100644 --- a/res/content/base/blocks/grass_block.json +++ b/res/content/base/blocks/grass_block.json @@ -8,5 +8,8 @@ "grass_side", "grass_side" ], - "base:durability": 1.3 + "base:durability": 1.3, + "base:loot": [ + {"item": "base:dirt.item"} + ] } diff --git a/res/content/base/config/user-props.toml b/res/content/base/config/user-props.toml index b7306ba1c..eab7a628b 100644 --- a/res/content/base/config/user-props.toml +++ b/res/content/base/config/user-props.toml @@ -1 +1,2 @@ "base:durability" = {} +"base:loot" = {} diff --git a/res/content/base/modules/util.lua b/res/content/base/modules/util.lua index 2112c2105..d12337fbe 100644 --- a/res/content/base/modules/util.lua +++ b/res/content/base/modules/util.lua @@ -11,4 +11,34 @@ function util.drop(ppos, itemid, count, pickup_delay) }}) end +local function calc_loot(loot_table) + local results = {} + for _, loot in ipairs(loot_table) do + local chance = loot.chance or 1 + local count = loot.count or 1 + + local roll = math.random() + + if roll < chance then + if loot.min and loot.max then + count = math.random(loot.min, loot.max) + end + if count == 0 then + goto continue + end + table.insert(results, {item=item.index(loot.item), count=count}) + end + ::continue:: + end + return results +end + +function util.block_loot(blockid) + local lootscheme = block.properties[blockid]["base:loot"] + if lootscheme then + return calc_loot(lootscheme) + end + return {{item=block.get_picking_item(blockid), count=1}} +end + return util diff --git a/res/content/base/scripts/components/falling_block.lua b/res/content/base/scripts/components/falling_block.lua index f650b6f79..483e86aee 100644 --- a/res/content/base/scripts/components/falling_block.lua +++ b/res/content/base/scripts/components/falling_block.lua @@ -3,17 +3,32 @@ local body = entity.rigidbody local rig = entity.skeleton local blockid = ARGS.block +local blockstates = ARGS.states or 0 if SAVED_DATA.block then blockid = SAVED_DATA.block + blockstates = SAVED_DATA.states or 0 else SAVED_DATA.block = blockid + SAVED_DATA.states = blockstates end do -- setup visuals - local textures = block.get_textures(block.index(blockid)) + local id = block.index(blockid) + local rotation = block.decompose_state(blockstates)[1] + local textures = block.get_textures(id) for i,t in ipairs(textures) do rig:set_texture("$"..tostring(i-1), "blocks:"..textures[i]) end + local axisX = {block.get_X(id, rotation)} + local axisY = {block.get_Y(id, rotation)} + local axisZ = {block.get_Z(id, rotation)} + local matrix = { + axisX[1], axisX[2], axisX[3], 0, + axisY[1], axisY[2], axisY[3], 0, + axisZ[1], axisZ[2], axisZ[3], 0, + 0, 0, 0, 1 + } + rig:set_matrix(0, matrix) end function on_grounded() @@ -22,7 +37,7 @@ function on_grounded() local iy = math.floor(pos[2]) local iz = math.floor(pos[3]) if block.is_replaceable_at(ix, iy, iz) then - block.place(ix, iy, iz, block.index(blockid), 0) + block.place(ix, iy, iz, block.index(blockid), blockstates) else local picking_item = block.get_picking_item(block.index(blockid)) local drop = entities.spawn("base:drop", pos, {base__drop={id=picking_item, count=1}}) diff --git a/res/content/base/scripts/world.lua b/res/content/base/scripts/world.lua index 62d111ae3..4b78a3f1d 100644 --- a/res/content/base/scripts/world.lua +++ b/res/content/base/scripts/world.lua @@ -1,12 +1,16 @@ function on_block_broken(id, x, y, z, playerid) - gfx.particles.emit({x+0.5, y+0.5, z+0.5}, 64, { - lifetime=1.0, - spawn_interval=0.0001, - explosion={4, 4, 4}, - texture="blocks:"..block.get_textures(id)[1], - random_sub_uv=0.1, - size={0.1, 0.1, 0.1}, - spawn_shape="box", - spawn_spread={0.4, 0.4, 0.4} - }) + if gfx then + gfx.particles.emit({x+0.5, y+0.5, z+0.5}, 64, { + lifetime=1.0, + spawn_interval=0.0001, + explosion={4, 4, 4}, + texture="blocks:"..block.get_textures(id)[1], + random_sub_uv=0.1, + size={0.1, 0.1, 0.1}, + spawn_shape="box", + spawn_spread={0.4, 0.4, 0.4} + }) + end + + rules.create("do-loot-non-player", true) end diff --git a/res/layouts/pages/content.xml.lua b/res/layouts/pages/content.xml.lua index d03c6e80e..0eae8c009 100644 --- a/res/layouts/pages/content.xml.lua +++ b/res/layouts/pages/content.xml.lua @@ -99,8 +99,14 @@ function refresh() end end + local packids = {unpack(packs_installed)} + for i,k in ipairs(packs_available) do + table.insert(packids, k) + end + local packinfos = pack.get_info(packids) + for i,id in ipairs(packs_installed) do - local packinfo = pack.get_info(id) + local packinfo = packinfos[id] packinfo.index = i callback = not table.has(base_packs, id) and string.format('move_pack("%s")', id) or nil packinfo.error = check_dependencies(packinfo) @@ -108,7 +114,7 @@ function refresh() end for i,id in ipairs(packs_available) do - local packinfo = pack.get_info(id) + local packinfo = packinfos[id] packinfo.index = i callback = string.format('move_pack("%s")', id) packinfo.error = check_dependencies(packinfo) diff --git a/res/layouts/pages/content_menu.xml.lua b/res/layouts/pages/content_menu.xml.lua index 6d730340b..3a1cea300 100644 --- a/res/layouts/pages/content_menu.xml.lua +++ b/res/layouts/pages/content_menu.xml.lua @@ -247,8 +247,9 @@ function refresh() local contents = document.contents contents:clear() + local packinfos = pack.get_info(packs_installed) for i, id in ipairs(packs_installed) do - local packinfo = pack.get_info(id) + local packinfo = packinfos[id] packinfo.id = id packs_installed[i] = {packinfo.id, packinfo.title} diff --git a/res/layouts/pages/main.xml b/res/layouts/pages/main.xml index 9c562fb44..a39606661 100644 --- a/res/layouts/pages/main.xml +++ b/res/layouts/pages/main.xml @@ -1,5 +1,6 @@ + diff --git a/res/layouts/pages/scripts.xml b/res/layouts/pages/scripts.xml new file mode 100644 index 000000000..d5c448038 --- /dev/null +++ b/res/layouts/pages/scripts.xml @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/res/layouts/pages/scripts.xml.lua b/res/layouts/pages/scripts.xml.lua new file mode 100644 index 000000000..259792b82 --- /dev/null +++ b/res/layouts/pages/scripts.xml.lua @@ -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 diff --git a/res/layouts/templates/script.xml b/res/layouts/templates/script.xml new file mode 100644 index 000000000..1ef770d8d --- /dev/null +++ b/res/layouts/templates/script.xml @@ -0,0 +1,3 @@ + diff --git a/res/modules/bit_converter.lua b/res/modules/bit_converter.lua index 71161eff1..b72dc3114 100644 --- a/res/modules/bit_converter.lua +++ b/res/modules/bit_converter.lua @@ -13,15 +13,15 @@ local MAX_INT64 = 9223372036854775807 local MIN_INT64 = -9223372036854775808 local function maskHighBytes(num) - return bit.band(num, 0xFF) + return bit.band(num, 0xFF) end local function reverse(tbl) - for i=1, math.floor(#tbl / 2) do - local tmp = tbl[i] - tbl[i] = tbl[#tbl - i + 1] - tbl[#tbl - i + 1] = tmp - end + for i=1, math.floor(#tbl / 2) do + local tmp = tbl[i] + tbl[i] = tbl[#tbl - i + 1] + tbl[#tbl - i + 1] = tmp + end return tbl end @@ -29,60 +29,60 @@ local orders = { "LE", "BE" } local fromLEConvertors = { - LE = function(bytes) return bytes end, - BE = function(bytes) return reverse(bytes) end + LE = function(bytes) return bytes end, + BE = function(bytes) return reverse(bytes) end } local toLEConvertors = { - LE = function(bytes) return bytes end, - BE = function(bytes) return reverse(bytes) end + LE = function(bytes) return bytes end, + BE = function(bytes) return reverse(bytes) end } -bit_converter.default_order = "LE" +bit_converter.default_order = "BE" local function fromLE(bytes, orderTo) - if orderTo then - bit_converter.validate_order(orderTo) - return fromLEConvertors[orderTo](bytes) - else return bytes end + if orderTo then + bit_converter.validate_order(orderTo) + return fromLEConvertors[orderTo](bytes) + else return bytes end end local function toLE(bytes, orderFrom) - if orderFrom then - bit_converter.validate_order(orderFrom) - return toLEConvertors[orderFrom](bytes) - else return bytes end + if orderFrom then + bit_converter.validate_order(orderFrom) + return toLEConvertors[orderFrom](bytes) + else return bytes end end function bit_converter.validate_order(order) - if not bit_converter.is_valid_order(order) then - error("invalid order: "..order) - end + if not bit_converter.is_valid_order(order) then + error("invalid order: "..order) + end end function bit_converter.is_valid_order(order) return table.has(orders, order) end function bit_converter.string_to_bytes(str) - local bytes = { } + local bytes = { } - local len = string.len(str) + local len = string.len(str) - local lenBytes = bit_converter.uint16_to_bytes(len) + local lenBytes = bit_converter.uint16_to_bytes(len) - for i = 1, #lenBytes do - bytes[i] = lenBytes[i] - end + for i = 1, #lenBytes do + bytes[i] = lenBytes[i] + end - for i = 1, len do - bytes[#bytes + 1] = string.byte(string.sub(str, i, i)) - end + for i = 1, len do + bytes[#bytes + 1] = string.byte(string.sub(str, i, i)) + end - return bytes + return bytes end function bit_converter.bool_to_byte(bool) - return bool and 1 or 0 + return bool and 1 or 0 end -- Credits to Iryont @@ -151,177 +151,178 @@ end -- function bit_converter.float32_to_bytes(float, order) - return fromLE(floatOrDoubleToBytes(float, 'f'), order) + return fromLE(floatOrDoubleToBytes(float, 'f'), order) end function bit_converter.float64_to_bytes(float, order) - return fromLE(floatOrDoubleToBytes(float, 'd'), order) + return fromLE(floatOrDoubleToBytes(float, 'd'), order) end function bit_converter.single_to_bytes(float, order) - on_deprecated_call("bit_converter.float_to_bytes", "bit_converter.float32_to_bytes") - return bit_converter.float32_to_bytes(bytes, order) + on_deprecated_call("bit_converter.float_to_bytes", "bit_converter.float32_to_bytes") + return bit_converter.float32_to_bytes(bytes, order) end function bit_converter.double_to_bytes(double, order) - on_deprecated_call("bit_converter.double_to_bytes", "bit_converter.float64_to_bytes") - return bit_converter.float64_to_bytes(bytes, order) + on_deprecated_call("bit_converter.double_to_bytes", "bit_converter.float64_to_bytes") + return bit_converter.float64_to_bytes(bytes, order) end local function uint32ToBytes(int, order) - return fromLE({ - maskHighBytes(bit.rshift(int, 24)), - maskHighBytes(bit.rshift(int, 16)), - maskHighBytes(bit.rshift(int, 8)), - maskHighBytes(int) - }, order) + return fromLE({ + maskHighBytes(int), + maskHighBytes(bit.rshift(int, 8)), + maskHighBytes(bit.rshift(int, 16)), + maskHighBytes(bit.rshift(int, 24)) + }, order) end local function uint16ToBytes(int, order) - return fromLE({ - maskHighBytes(bit.rshift(int, 8)), - maskHighBytes(int) - }, order) + return fromLE({ + maskHighBytes(int), + maskHighBytes(bit.rshift(int, 8)) + }, order) end function bit_converter.uint32_to_bytes(int, order) - if int > MAX_UINT32 or int < MIN_UINT32 then - error("invalid uint32") - end + if int > MAX_UINT32 or int < MIN_UINT32 then + error("invalid uint32") + end - return uint32ToBytes(int, order) + return uint32ToBytes(int, order) end function bit_converter.uint16_to_bytes(int, order) - if int > MAX_UINT16 or int < MIN_UINT16 then - error("invalid uint16") - end + if int > MAX_UINT16 or int < MIN_UINT16 then + error("invalid uint16") + end - return uint16ToBytes(int, order) + return uint16ToBytes(int, order) end function bit_converter.int64_to_bytes(int, order) - if int > MAX_INT64 or int < MIN_INT64 then - error("invalid int64") - end - - return fromLE({ - maskHighBytes(bit.rshift(int, 56)), - maskHighBytes(bit.rshift(int, 48)), - maskHighBytes(bit.rshift(int, 40)), - maskHighBytes(bit.rshift(int, 32)), - maskHighBytes(bit.rshift(int, 24)), - maskHighBytes(bit.rshift(int, 16)), - maskHighBytes(bit.rshift(int, 8)), - maskHighBytes(int) - }, order) + if int > MAX_INT64 or int < MIN_INT64 then + error("invalid int64") + end + + return fromLE({ + maskHighBytes(int), + maskHighBytes(bit.rshift(int, 8)), + maskHighBytes(bit.rshift(int, 16)), + maskHighBytes(bit.rshift(int, 24)), + maskHighBytes(bit.rshift(int, 32)), + maskHighBytes(bit.rshift(int, 40)), + maskHighBytes(bit.rshift(int, 48)), + maskHighBytes(bit.rshift(int, 56)) + }, order) end function bit_converter.int32_to_bytes(int, order) - on_deprecated_call("bit_converter.int32_to_bytes", "bit_converter.sint32_to_bytes") + on_deprecated_call("bit_converter.int32_to_bytes", "bit_converter.sint32_to_bytes") - if int > MAX_INT32 or int < MIN_INT32 then - error("invalid int32") - end + if int > MAX_INT32 or int < MIN_INT32 then + error("invalid int32") + end - return uint32ToBytes(int + MAX_INT32, order) + return uint32ToBytes(int + MAX_INT32, order) end function bit_converter.int16_to_bytes(int, order) - on_deprecated_call("bit_converter.int32_to_bytes", "bit_converter.sint16_to_bytes") + on_deprecated_call("bit_converter.int32_to_bytes", "bit_converter.sint16_to_bytes") - if int > MAX_INT16 or int < MIN_INT16 then - error("invalid int16") - end + if int > MAX_INT16 or int < MIN_INT16 then + error("invalid int16") + end - return uint16ToBytes(int + MAX_INT16, order) + return uint16ToBytes(int + MAX_INT16, order) end function bit_converter.sint32_to_bytes(int, order) - if int > MAX_INT32 or int < MIN_INT32 then - error("invalid sint32") - end + if int > MAX_INT32 or int < MIN_INT32 then + error("invalid sint32") + end - return uint32ToBytes(int + MAX_UINT32 + 1, order) + return uint32ToBytes(int + MAX_UINT32 + 1, order) end function bit_converter.sint16_to_bytes(int, order) - if int > MAX_INT16 or int < MIN_INT16 then - error("invalid sint16") - end + if int > MAX_INT16 or int < MIN_INT16 then + error("invalid sint16") + end - return uint16ToBytes(int + MAX_UINT16 + 1, order) + return uint16ToBytes(int + MAX_UINT16 + 1, order) end function bit_converter.bytes_to_float32(bytes, order) - return bytesToFloatOrDouble(toLE(bytes, order), 'f') + return bytesToFloatOrDouble(toLE(bytes, order), 'f') end function bit_converter.bytes_to_float64(bytes, order) - return bytesToFloatOrDouble(toLE(bytes, order), 'd') + return bytesToFloatOrDouble(toLE(bytes, order), 'd') end function bit_converter.bytes_to_single(bytes, order) - on_deprecated_call("bit_converter.bytes_to_single", "bit_converter.bytes_to_float32") - return bit_converter.bytes_to_float32(bytes, order) + on_deprecated_call("bit_converter.bytes_to_single", "bit_converter.bytes_to_float32") + return bit_converter.bytes_to_float32(bytes, order) end function bit_converter.bytes_to_double(bytes, order) - on_deprecated_call("bit_converter.bytes_to_double", "bit_converter.bytes_to_float64") - return bit_converter.bytes_to_float64(bytes, order) + on_deprecated_call("bit_converter.bytes_to_double", "bit_converter.bytes_to_float64") + return bit_converter.bytes_to_float64(bytes, order) end function bit_converter.bytes_to_string(bytes, order) - local len = bit_converter.bytes_to_uint16({ bytes[1], bytes[2] }) + local len = bit_converter.bytes_to_uint16({ bytes[1], bytes[2] }) - local str = "" + local str = "" - for i = 1, len do - str = str..string.char(bytes[i + 2]) - end + for i = 1, len do + str = str..string.char(bytes[i + 2]) + end - return str + return str end function bit_converter.byte_to_bool(byte) - return byte ~= 0 + return byte ~= 0 end function bit_converter.bytes_to_uint32(bytes, order) - if #bytes < 4 then - error("eof") - end + if #bytes < 4 then + error("eof") + end - bytes = toLE(bytes, order) + bytes = toLE(bytes, order) return bit.bor( bit.bor( bit.bor( - bit.lshift(bytes[1], 24), - bit.lshift(bytes[2], 16)), - bit.lshift(bytes[3], 8)),bytes[4]) + bytes[1], + bit.lshift(bytes[2], 8)), + bit.lshift(bytes[3], 16)), + bit.lshift(bytes[4], 24)) end function bit_converter.bytes_to_uint16(bytes, order) - if #bytes < 2 then - error("eof") - end + if #bytes < 2 then + error("eof") + end - bytes = toLE(bytes, order) + bytes = toLE(bytes, order) return bit.bor( - bit.lshift(bytes[1], 8), - bytes[2], 0) + bit.lshift(bytes[2], 8), + bytes[1], 0) end function bit_converter.bytes_to_int64(bytes, order) - if #bytes < 8 then - error("eof") - end + if #bytes < 8 then + error("eof") + end - bytes = toLE(bytes, order) + bytes = toLE(bytes, order) return bit.bor( @@ -331,35 +332,35 @@ function bit_converter.bytes_to_int64(bytes, order) bit.bor( bit.bor( bit.bor( - bit.lshift(bytes[1], 56), - bit.lshift(bytes[2], 48)), - bit.lshift(bytes[3], 40)), - bit.lshift(bytes[4], 32)), - bit.lshift(bytes[5], 24)), - bit.lshift(bit.band(bytes[6], 0xFF), 16)), - bit.lshift(bit.band(bytes[7], 0xFF), 8)),bit.band(bytes[8], 0xFF)) + bit.lshift(bytes[8], 56), + bit.lshift(bytes[7], 48)), + bit.lshift(bytes[6], 40)), + bit.lshift(bytes[5], 32)), + bit.lshift(bytes[4], 24)), + bit.lshift(bit.band(bytes[3], 0xFF), 16)), + bit.lshift(bit.band(bytes[2], 0xFF), 8)),bit.band(bytes[1], 0xFF)) end function bit_converter.bytes_to_int32(bytes, order) - on_deprecated_call("bit_converter.bytes_to_int32", "bit_converter.bytes_to_sint32") - return bit_converter.bytes_to_uint32(bytes, order) - MAX_INT32 + on_deprecated_call("bit_converter.bytes_to_int32", "bit_converter.bytes_to_sint32") + return bit_converter.bytes_to_uint32(bytes, order) - MAX_INT32 end function bit_converter.bytes_to_int16(bytes, order) - on_deprecated_call("bit_converter.bytes_to_int16", "bit_converter.bytes_to_sint16") - return bit_converter.bytes_to_uint16(bytes, order) - MAX_INT16 + on_deprecated_call("bit_converter.bytes_to_int16", "bit_converter.bytes_to_sint16") + return bit_converter.bytes_to_uint16(bytes, order) - MAX_INT16 end function bit_converter.bytes_to_sint32(bytes, order) - local num = bit_converter.bytes_to_uint32(bytes, order) + local num = bit_converter.bytes_to_uint32(bytes, order) - return MIN_INT32 * (bit.band(MAX_INT32 + 1, num) ~= 0 and 1 or 0) + bit.band(MAX_INT32, num) + return MIN_INT32 * (bit.band(MAX_INT32 + 1, num) ~= 0 and 1 or 0) + bit.band(MAX_INT32, num) end function bit_converter.bytes_to_sint16(bytes, order) - local num = bit_converter.bytes_to_uint16(bytes, order) + local num = bit_converter.bytes_to_uint16(bytes, order) - return MIN_INT16 * (bit.band(MAX_INT16 + 1, num) ~= 0 and 1 or 0) + bit.band(MAX_INT16, num) + return MIN_INT16 * (bit.band(MAX_INT16 + 1, num) ~= 0 and 1 or 0) + bit.band(MAX_INT16, num) end return bit_converter diff --git a/res/modules/gui_util.lua b/res/modules/gui_util.lua new file mode 100644 index 000000000..d29837653 --- /dev/null +++ b/res/modules/gui_util.lua @@ -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 diff --git a/res/modules/tests_util.lua b/res/modules/tests_util.lua new file mode 100644 index 000000000..1e2207c24 --- /dev/null +++ b/res/modules/tests_util.lua @@ -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 diff --git a/res/preload.json b/res/preload.json index 5efe0162f..fb202291e 100644 --- a/res/preload.json +++ b/res/preload.json @@ -31,5 +31,8 @@ "blocks", "items", "particles" + ], + "models": [ + "block" ] } diff --git a/res/scripts/classes.lua b/res/scripts/classes.lua index 909717cf1..0741c89b1 100644 --- a/res/scripts/classes.lua +++ b/res/scripts/classes.lua @@ -40,6 +40,7 @@ local Socket = {__index={ send=function(self, ...) return network.__send(self.id, ...) end, recv=function(self, ...) return network.__recv(self.id, ...) end, close=function(self) return network.__close(self.id) end, + available=function(self) return network.__available(self.id) or 0 end, is_alive=function(self) return network.__is_alive(self.id) end, is_connected=function(self) return network.__is_connected(self.id) end, get_address=function(self) return network.__get_address(self.id) end, diff --git a/res/scripts/hud.lua b/res/scripts/hud.lua new file mode 100644 index 000000000..093d43c32 --- /dev/null +++ b/res/scripts/hud.lua @@ -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 diff --git a/res/scripts/post_content.lua b/res/scripts/post_content.lua index ae67c6789..8725ed974 100644 --- a/res/scripts/post_content.lua +++ b/res/scripts/post_content.lua @@ -7,7 +7,7 @@ local names = { "hidden", "draw-group", "picking-item", "surface-replacement", "script-name", "ui-layout", "inventory-size", "tick-interval", "overlay-texture", "translucent", "fields", "particles", "icon-type", "icon", "placing-block", - "stack-size" + "stack-size", "name" } for name, _ in pairs(user_props) do table.insert(names, name) @@ -40,3 +40,24 @@ make_read_only(block.properties) for k,v in pairs(block.properties) do make_read_only(v) end + +local function cache_names(library) + local indices = {} + local names = {} + for id=0,library.defs_count()-1 do + local name = library.properties[id].name + indices[name] = id + names[id] = name + end + + function library.name(id) + return names[id] + end + + function library.index(name) + return indices[name] + end +end + +cache_names(block) +cache_names(item) diff --git a/res/scripts/stdlib.lua b/res/scripts/stdlib.lua index 9cf23cc3f..f376e2106 100644 --- a/res/scripts/stdlib.lua +++ b/res/scripts/stdlib.lua @@ -9,6 +9,87 @@ function sleep(timesec) end end +function tb_frame_tostring(frame) + local s = frame.short_src + if frame.what ~= "C" then + s = s .. ":" .. tostring(frame.currentline) + end + if frame.what == "main" then + s = s .. ": in main chunk" + elseif frame.name then + s = s .. ": in function " .. utf8.escape(frame.name) + end + return s +end + +local function complete_app_lib(app) + app.sleep = sleep + app.script = __VC_SCRIPT_NAME + app.new_world = core.new_world + app.open_world = core.open_world + app.save_world = core.save_world + app.close_world = core.close_world + app.reopen_world = core.reopen_world + app.delete_world = core.delete_world + app.reconfig_packs = core.reconfig_packs + app.get_setting = core.get_setting + app.set_setting = core.set_setting + app.tick = coroutine.yield + app.get_version = core.get_version + app.get_setting_info = core.get_setting_info + app.load_content = core.load_content + app.reset_content = core.reset_content + + function app.config_packs(packs_list) + -- Check if packs are valid and add dependencies to the configuration + packs_list = pack.assemble(packs_list) + + local installed = pack.get_installed() + local toremove = {} + for _, packid in ipairs(installed) do + if not table.has(packs_list, packid) then + table.insert(toremove, packid) + end + end + local toadd = {} + for _, packid in ipairs(packs_list) do + if not table.has(installed, packid) then + table.insert(toadd, packid) + end + end + app.reconfig_packs(toadd, toremove) + end + + function app.quit() + local tb = debug.get_traceback(1) + local s = "app.quit() traceback:" + for i, frame in ipairs(tb) do + s = s .. "\n\t"..tb_frame_tostring(frame) + end + debug.log(s) + core.quit() + coroutine.yield() + end + + function app.sleep_until(predicate, max_ticks) + max_ticks = max_ticks or 1e9 + local ticks = 0 + while ticks < max_ticks and not predicate() do + app.tick() + ticks = ticks + 1 + end + if ticks == max_ticks then + error("max ticks exceed") + end + end +end + +if app then + complete_app_lib(app) +elseif __vc_app then + complete_app_lib(__vc_app) +end + ------------------------------------------------ ------------------- Events --------------------- ------------------------------------------------ @@ -54,7 +135,12 @@ function events.emit(event, ...) return nil end for _, func in ipairs(handlers) do - result = result or func(...) + local status, newres = xpcall(func, __vc__error, ...) + if not status then + debug.error("error in event ("..event..") handler: "..newres) + else + result = result or newres + end end return result end @@ -85,7 +171,7 @@ function Document.new(docname) end local _RadioGroup = {} -function _RadioGroup.set(self, key) +function _RadioGroup:set(key) if type(self) ~= 'table' then error("called as non-OOP via '.', use radiogroup:set") end @@ -98,7 +184,7 @@ function _RadioGroup.set(self, key) self.callback(key) end end -function _RadioGroup.__call(self, elements, onset, default) +function _RadioGroup:__call(elements, onset, default) local group = setmetatable({ elements=elements, callback=onset, @@ -114,20 +200,8 @@ _GUI_ROOT = Document.new("core:root") _MENU = _GUI_ROOT.menu menu = _MENU -local __post_runnables = {} - -function __process_post_runnables() - if #__post_runnables then - for _, func in ipairs(__post_runnables) do - func() - end - __post_runnables = {} - end -end - -function time.post_runnable(runnable) - table.insert(__post_runnables, runnable) -end +local gui_util = require "core:gui_util" +__vc_page_loader = gui_util.load_page --- Console library extension --- console.cheats = {} @@ -272,7 +346,6 @@ function __vc_on_hud_open() _rules.create("allow-content-access", hud._is_content_access(), function(value) hud._set_content_access(value) - input.set_enabled("player.pick", value) end) _rules.create("allow-flight", true, function(value) input.set_enabled("player.flight", value) @@ -328,6 +401,86 @@ function __vc_on_world_quit() _rules.clear() end +local __vc_coroutines = {} +local __vc_named_coroutines = {} +local __vc_next_coroutine = 1 +local __vc_coroutine_error = nil + +function __vc_start_coroutine(chunk) + local co = coroutine.create(function() + local status, err = pcall(chunk) + if not status then + __vc_coroutine_error = err + end + end) + local id = __vc_next_coroutine + __vc_next_coroutine = __vc_next_coroutine + 1 + __vc_coroutines[id] = co + return id +end + +function __vc_resume_coroutine(id) + local co = __vc_coroutines[id] + if co then + coroutine.resume(co) + if __vc_coroutine_error then + debug.error(__vc_coroutine_error) + error(__vc_coroutine_error) + end + return coroutine.status(co) ~= "dead" + end + return false +end + +function __vc_stop_coroutine(id) + local co = __vc_coroutines[id] + if co then + if coroutine.close then + coroutine.close(co) + end + __vc_coroutines[id] = nil + end +end + +function start_coroutine(chunk, name) + local co = coroutine.create(function() + local status, error = xpcall(chunk, __vc__error) + if not status then + debug.error(error) + end + end) + __vc_named_coroutines[name] = co +end + +local __post_runnables = {} + +function __process_post_runnables() + if #__post_runnables then + for _, func in ipairs(__post_runnables) do + local status, result = xpcall(func, __vc__error) + if not status then + debug.error("error in post_runnable: "..result) + end + end + __post_runnables = {} + end + + local dead = {} + for name, co in pairs(__vc_named_coroutines) do + coroutine.resume(co) + if coroutine.status(co) == "dead" then + table.insert(dead, name) + end + end + for _, name in ipairs(dead) do + __vc_named_coroutines[name] = nil + end +end + +function time.post_runnable(runnable) + table.insert(__post_runnables, runnable) +end + assets = {} assets.load_texture = core.__load_texture diff --git a/res/scripts/stdmin.lua b/res/scripts/stdmin.lua index 5b93fa2f9..5da1b3474 100644 --- a/res/scripts/stdmin.lua +++ b/res/scripts/stdmin.lua @@ -34,11 +34,11 @@ end function timeit(iters, func, ...) - local tm = time.uptime() + local tm = os.clock() for i=1,iters do func(...) end - print("[time mcs]", (time.uptime()-tm) * 1000000) + print("[time mcs]", (os.clock()-tm) * 1000000) end ---------------------------------------------- @@ -361,3 +361,20 @@ function __vc_warning(msg, detail, n) "core:warning", msg, detail, debug.get_traceback(1 + (n or 0))) end end + +function file.name(path) + return path:match("([^:/\\]+)$") +end + +function file.stem(path) + local name = file.name(path) + return name:match("(.+)%.[^%.]+$") or name +end + +function file.ext(path) + return path:match("%.([^:/\\]+)$") +end + +function file.prefix(path) + return path:match("^([^:]+)") +end diff --git a/res/texts/ru_RU.txt b/res/texts/ru_RU.txt index e78b5f729..d3c408935 100644 --- a/res/texts/ru_RU.txt +++ b/res/texts/ru_RU.txt @@ -34,6 +34,7 @@ graphics.dense-render.tooltip=Включает прозрачность блок menu.Apply=Применить menu.Audio=Звук menu.Back to Main Menu=Вернуться в Меню +menu.Scripts=Сценарии menu.Content Error=Ошибка Контента menu.Content=Контент menu.Continue=Продолжить diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7eecaef99..6350f4b88 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -4,15 +4,19 @@ set(CMAKE_CXX_STANDARD 17) file(GLOB_RECURSE HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/*.hpp) file(GLOB_RECURSE SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp) -list(REMOVE_ITEM SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/voxel_engine.cpp) +list(REMOVE_ITEM SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp) -add_library(${PROJECT_NAME} ${SOURCES} ${HEADERS}) - -option(VOXELENGINE_BUILD_WINDOWS_VCPKG ON) +add_library(${PROJECT_NAME} STATIC ${SOURCES} ${HEADERS}) find_package(OpenGL REQUIRED) find_package(GLEW REQUIRED) -find_package(OpenAL REQUIRED) +if (CMAKE_SYSTEM_NAME STREQUAL "Windows") + # specific for vcpkg + find_package(OpenAL CONFIG REQUIRED) + set(OPENAL_LIBRARY OpenAL::OpenAL) +else() + find_package(OpenAL REQUIRED) +endif() find_package(ZLIB REQUIRED) find_package(PNG REQUIRED) find_package(CURL REQUIRED) @@ -20,19 +24,23 @@ if (NOT APPLE) find_package(EnTT REQUIRED) endif() -if (WIN32) - if(VOXELENGINE_BUILD_WINDOWS_VCPKG) - set(LUA_LIBRARIES "${CMAKE_CURRENT_SOURCE_DIR}/../vcpkg/packages/luajit_x64-windows/lib/lua51.lib") - set(LUA_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../vcpkg/packages/luajit_x64-windows/include/luajit") - find_package(glfw3 REQUIRED) - find_package(glm REQUIRED) - find_package(vorbis REQUIRED) - set(VORBISLIB Vorbis::vorbis Vorbis::vorbisfile) +set(LIBS "") + +if (CMAKE_SYSTEM_NAME STREQUAL "Windows") + # Use directly linking to lib instead PkgConfig (because pkg-config dont install on windows as default) + # TODO: Do it with findLua. + if (MSVC) + set(LUA_INCLUDE_DIR "$ENV{VCPKG_ROOT}/packages/luajit_${VCPKG_TARGET_TRIPLET}/include/luajit") + find_package(Lua REQUIRED) else() - find_package(Lua REQUIRED) - set(VORBISLIB vorbis vorbisfile) # not tested - add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/libs/glfw) + # Used for mingw-clang cross compiling from msys2 + set(LIBS ${LIBS} luajit-5.1) endif() + find_package(glfw3 REQUIRED) + find_package(glm REQUIRED) + find_package(vorbis REQUIRED) + set(VORBISLIB Vorbis::vorbis Vorbis::vorbisfile) + elseif(APPLE) find_package(PkgConfig) pkg_check_modules(LUAJIT REQUIRED luajit) @@ -53,8 +61,6 @@ else() set(VORBISLIB ${VORBIS_LDFLAGS}) endif() -set(LIBS "") - if(UNIX) find_package(glfw3 3.3 REQUIRED) find_package(Threads REQUIRED) diff --git a/src/audio/audio.cpp b/src/audio/audio.cpp index 0dc0f591c..522e37053 100644 --- a/src/audio/audio.cpp +++ b/src/audio/audio.cpp @@ -8,6 +8,9 @@ #include "coders/wav.hpp" #include "AL/ALAudio.hpp" #include "NoAudio.hpp" +#include "debug/Logger.hpp" + +static debug::Logger logger("audio"); namespace audio { static speakerid_t nextId = 1; @@ -147,10 +150,14 @@ class PCMVoidSource : public PCMStream { void audio::initialize(bool enabled) { if (enabled) { + logger.info() << "initializing ALAudio backend"; backend = ALAudio::create().release(); } if (backend == nullptr) { - std::cerr << "could not to initialize audio" << std::endl; + if (enabled) { + std::cerr << "could not to initialize audio" << std::endl; + } + logger.info() << "initializing NoAudio backend"; backend = NoAudio::create().release(); } create_channel("master"); diff --git a/src/coders/byte_utils.cpp b/src/coders/byte_utils.cpp index e30c14f2e..bd4a00909 100644 --- a/src/coders/byte_utils.cpp +++ b/src/coders/byte_utils.cpp @@ -6,6 +6,10 @@ #include "util/data_io.hpp" +ByteBuilder::ByteBuilder(size_t size) { + buffer.reserve(size); +} + void ByteBuilder::put(ubyte b) { buffer.push_back(b); } @@ -31,37 +35,37 @@ void ByteBuilder::put(const ubyte* arr, size_t size) { } } -void ByteBuilder::putInt16(int16_t val) { +void ByteBuilder::putInt16(int16_t val, bool bigEndian) { size_t size = buffer.size(); buffer.resize(buffer.size() + sizeof(int16_t)); - val = dataio::h2le(val); + val = bigEndian ? dataio::h2be(val) : dataio::h2le(val); std::memcpy(buffer.data()+size, &val, sizeof(int16_t)); } -void ByteBuilder::putInt32(int32_t val) { +void ByteBuilder::putInt32(int32_t val, bool bigEndian) { size_t size = buffer.size(); buffer.resize(buffer.size() + sizeof(int32_t)); - val = dataio::h2le(val); + val = bigEndian ? dataio::h2be(val) : dataio::h2le(val); std::memcpy(buffer.data()+size, &val, sizeof(int32_t)); } -void ByteBuilder::putInt64(int64_t val) { +void ByteBuilder::putInt64(int64_t val, bool bigEndian) { size_t size = buffer.size(); buffer.resize(buffer.size() + sizeof(int64_t)); - val = dataio::h2le(val); + val = bigEndian ? dataio::h2be(val) : dataio::h2le(val); std::memcpy(buffer.data()+size, &val, sizeof(int64_t)); } -void ByteBuilder::putFloat32(float val) { +void ByteBuilder::putFloat32(float val, bool bigEndian) { int32_t i32_val; std::memcpy(&i32_val, &val, sizeof(int32_t)); - putInt32(i32_val); + putInt32(i32_val, bigEndian); } -void ByteBuilder::putFloat64(double val) { +void ByteBuilder::putFloat64(double val, bool bigEndian) { int64_t i64_val; std::memcpy(&i64_val, &val, sizeof(int64_t)); - putInt64(i64_val); + putInt64(i64_val, bigEndian); } void ByteBuilder::set(size_t position, ubyte val) { @@ -95,6 +99,10 @@ ByteReader::ByteReader(const ubyte* data) : data(data), size(4), pos(0) { size = getInt32(); } +ByteReader::ByteReader(const std::vector& data) + : data(data.data()), size(data.size()), pos(0) { +} + void ByteReader::checkMagic(const char* data, size_t size) { if (pos + size >= this->size) { throw std::runtime_error("invalid magic number"); @@ -129,45 +137,51 @@ ubyte ByteReader::peek() { return data[pos]; } -int16_t ByteReader::getInt16() { +int16_t ByteReader::getInt16(bool bigEndian) { if (pos + sizeof(int16_t) > size) { throw std::runtime_error("buffer underflow"); } int16_t value; std::memcpy(&value, data + pos, sizeof(int16_t)); pos += sizeof(int16_t); - return dataio::le2h(value); + return bigEndian ? dataio::be2h(value) : dataio::le2h(value); } -int32_t ByteReader::getInt32() { +int32_t ByteReader::getInt32(bool bigEndian) { if (pos + sizeof(int32_t) > size) { throw std::runtime_error("buffer underflow"); } int32_t value; std::memcpy(&value, data + pos, sizeof(int32_t)); pos += sizeof(int32_t); - return dataio::le2h(value); + return bigEndian ? dataio::be2h(value) : dataio::le2h(value); } -int64_t ByteReader::getInt64() { +int64_t ByteReader::getInt64(bool bigEndian) { if (pos + sizeof(int64_t) > size) { throw std::runtime_error("buffer underflow"); } int64_t value; std::memcpy(&value, data + pos, sizeof(int64_t)); pos += sizeof(int64_t); - return dataio::le2h(value); + return bigEndian ? dataio::be2h(value) : dataio::le2h(value); } -float ByteReader::getFloat32() { +float ByteReader::getFloat32(bool bigEndian) { int32_t i32_val = getInt32(); + if (bigEndian) { + i32_val = dataio::be2h(i32_val); + } float val; std::memcpy(&val, &i32_val, sizeof(float)); return val; } -double ByteReader::getFloat64() { +double ByteReader::getFloat64(bool bigEndian) { int64_t i64_val = getInt64(); + if (bigEndian) { + i64_val = dataio::be2h(i64_val); + } double val; std::memcpy(&val, &i64_val, sizeof(double)); return val; diff --git a/src/coders/byte_utils.hpp b/src/coders/byte_utils.hpp index 0532fe43a..8d98585a3 100644 --- a/src/coders/byte_utils.hpp +++ b/src/coders/byte_utils.hpp @@ -8,20 +8,23 @@ class ByteBuilder { std::vector buffer; public: + ByteBuilder() = default; + ByteBuilder(size_t size); + /// @brief Write one byte (8 bit unsigned integer) void put(ubyte b); /// @brief Write c-string (bytes array terminated with '\00') void putCStr(const char* str); /// @brief Write signed 16 bit little-endian integer - void putInt16(int16_t val); + void putInt16(int16_t val, bool bigEndian = false); /// @brief Write signed 32 bit integer - void putInt32(int32_t val); + void putInt32(int32_t val, bool bigEndian = false); /// @brief Write signed 64 bit integer - void putInt64(int64_t val); + void putInt64(int64_t val, bool bigEndian = false); /// @brief Write 32 bit floating-point number - void putFloat32(float val); + void putFloat32(float val, bool bigEndian = false); /// @brief Write 64 bit floating-point number - void putFloat64(double val); + void putFloat64(double val, bool bigEndian = false); /// @brief Write string (uint32 length + bytes) void put(const std::string& s); @@ -50,6 +53,7 @@ class ByteReader { public: ByteReader(const ubyte* data, size_t size); ByteReader(const ubyte* data); + ByteReader(const std::vector& data); void checkMagic(const char* data, size_t size); /// @brief Get N bytes @@ -59,15 +63,15 @@ class ByteReader { /// @brief Read one byte (unsigned 8 bit integer) without pointer move ubyte peek(); /// @brief Read signed 16 bit little-endian integer - int16_t getInt16(); + int16_t getInt16(bool bigEndian = false); /// @brief Read signed 32 bit little-endian integer - int32_t getInt32(); + int32_t getInt32(bool bigEndian = false); /// @brief Read signed 64 bit little-endian integer - int64_t getInt64(); + int64_t getInt64(bool bigEndian = false); /// @brief Read 32 bit floating-point number - float getFloat32(); + float getFloat32(bool bigEndian = false); /// @brief Read 64 bit floating-point number - double getFloat64(); + double getFloat64(bool bigEndian = false); /// @brief Read C-String const char* getCString(); /// @brief Read string with unsigned 32 bit number before (length) diff --git a/src/coders/xml.cpp b/src/coders/xml.cpp index b2cb88092..84cc9f375 100644 --- a/src/coders/xml.cpp +++ b/src/coders/xml.cpp @@ -92,11 +92,11 @@ glm::vec4 Attribute::asColor() const { throw std::runtime_error("#RRGGBB or #RRGGBBAA required"); } int a = 255; - int r = (hexchar2int(text[1]) << 4) | hexchar2int(text[2]); - int g = (hexchar2int(text[3]) << 4) | hexchar2int(text[4]); - int b = (hexchar2int(text[5]) << 4) | hexchar2int(text[6]); + int r = (std::max(0, hexchar2int(text[1])) << 4) | hexchar2int(text[2]); + int g = (std::max(0, hexchar2int(text[3])) << 4) | hexchar2int(text[4]); + int b = (std::max(0, hexchar2int(text[5])) << 4) | hexchar2int(text[6]); if (text.length() == 9) { - a = (hexchar2int(text[7]) << 4) | hexchar2int(text[8]); + a = (std::max(0, hexchar2int(text[7])) << 4) | hexchar2int(text[8]); } return glm::vec4(r / 255.f, g / 255.f, b / 255.f, a / 255.f); } else { diff --git a/src/constants.hpp b/src/constants.hpp index c8de5a8dd..6f4a6ce6e 100644 --- a/src/constants.hpp +++ b/src/constants.hpp @@ -35,6 +35,11 @@ inline constexpr int CHUNK_D = 16; inline constexpr uint VOXEL_USER_BITS = 8; inline constexpr uint VOXEL_USER_BITS_OFFSET = sizeof(blockstate_t)*8-VOXEL_USER_BITS; +/// @brief % unordered map max average buckets load factor. +/// Low value gives significant performance impact by minimizing collisions and +/// lookup latency. Default value (1.0) shows x2 slower work. +inline constexpr float CHUNKS_MAP_MAX_LOAD_FACTOR = 0.1f; + /// @brief chunk volume (count of voxels per Chunk) inline constexpr int CHUNK_VOL = (CHUNK_W * CHUNK_H * CHUNK_D); diff --git a/src/content/ContentBuilder.cpp b/src/content/ContentBuilder.cpp index f7ab3b1a7..49433dfd1 100644 --- a/src/content/ContentBuilder.cpp +++ b/src/content/ContentBuilder.cpp @@ -91,10 +91,18 @@ std::unique_ptr ContentBuilder::build() { for (Block* def : blockDefsIndices) { def->rt.pickingItem = content->items.require(def->pickingItem).rt.id; def->rt.surfaceReplacement = content->blocks.require(def->surfaceReplacement).rt.id; + if (def->properties == nullptr) { + def->properties = dv::object(); + def->properties["name"] = def->name; + } } for (ItemDef* def : itemDefsIndices) { def->rt.placingBlock = content->blocks.require(def->placingBlock).rt.id; + if (def->properties == nullptr) { + def->properties = dv::object(); + } + def->properties["name"] = def->name; } for (auto& [name, def] : content->generators.getDefs()) { diff --git a/src/content/ContentBuilder.hpp b/src/content/ContentBuilder.hpp index 44099304c..cc35838ce 100644 --- a/src/content/ContentBuilder.hpp +++ b/src/content/ContentBuilder.hpp @@ -35,11 +35,13 @@ class ContentUnitBuilder { : allNames(allNames), type(type) { } - T& create(const std::string& id) { + T& create(const std::string& id, bool* created = nullptr) { auto found = defs.find(id); if (found != defs.end()) { + if (created) *created = false; return *found->second; } + if (created) *created = true; checkIdentifier(id); allNames[id] = type; names.push_back(id); diff --git a/src/content/ContentLoader.cpp b/src/content/ContentLoader.cpp index 1dc676ea4..ce3f50605 100644 --- a/src/content/ContentLoader.cpp +++ b/src/content/ContentLoader.cpp @@ -187,11 +187,49 @@ static void perform_user_block_fields( layout = StructLayout::create(fields); } +static void process_method( + dv::value& properties, + const std::string& method, + const std::string& name, + const dv::value& value +) { + if (method == "append") { + if (!properties.has(name)) { + properties[name] = dv::list(); + } + auto& list = properties[name]; + if (value.isList()) { + for (const auto& item : value) { + list.add(item); + } + } else { + list.add(value); + } + } else { + throw std::runtime_error( + "unknown method " + method + " for " + name + ); + } +} + void ContentLoader::loadBlock( Block& def, const std::string& name, const fs::path& file ) { auto root = files::read_json(file); - def.properties = root; + if (def.properties == nullptr) { + def.properties = dv::object(); + def.properties["name"] = name; + } + for (auto& [key, value] : root.asObject()) { + auto pos = key.rfind('@'); + if (pos == std::string::npos) { + def.properties[key] = value; + continue; + } + auto field = key.substr(0, pos); + auto suffix = key.substr(pos + 1); + process_method(def.properties, suffix, field, value); + } if (root.has("parent")) { const auto& parentName = root["parent"].asString(); @@ -492,7 +530,8 @@ void ContentLoader::loadBlock( if (fs::exists(configFile)) loadBlock(def, full, configFile); if (!def.hidden) { - auto& item = builder.items.create(full + BLOCK_ITEM_SUFFIX); + bool created; + auto& item = builder.items.create(full + BLOCK_ITEM_SUFFIX, &created); item.generated = true; item.caption = def.caption; item.iconType = ItemIconType::BLOCK; @@ -502,7 +541,7 @@ void ContentLoader::loadBlock( for (uint j = 0; j < 4; j++) { item.emission[j] = def.emission[j]; } - stats->totalItems++; + stats->totalItems += created; } } @@ -564,9 +603,10 @@ void ContentLoader::loadContent(const dv::value& root) { if (parent.empty() || builder.blocks.get(parent)) { // No dependency or dependency already loaded/exists in another // content pack - auto& def = builder.blocks.create(full); + bool created; + auto& def = builder.blocks.create(full, &created); loadBlock(def, full, name); - stats->totalBlocks++; + stats->totalBlocks += created; } else { // Dependency not loaded yet, add to pending items pendingDefs.emplace_back(full, name); @@ -583,9 +623,10 @@ void ContentLoader::loadContent(const dv::value& root) { if (builder.blocks.get(parent)) { // Dependency resolved or parent exists in another pack, // load the item - auto& def = builder.blocks.create(it->first); + bool created; + auto& def = builder.blocks.create(it->first, &created); loadBlock(def, it->first, it->second); - stats->totalBlocks++; + stats->totalBlocks += created; it = pendingDefs.erase(it); // Remove resolved item progressMade = true; } else { @@ -609,9 +650,10 @@ void ContentLoader::loadContent(const dv::value& root) { if (parent.empty() || builder.items.get(parent)) { // No dependency or dependency already loaded/exists in another // content pack - auto& def = builder.items.create(full); + bool created; + auto& def = builder.items.create(full, &created); loadItem(def, full, name); - stats->totalItems++; + stats->totalItems += created; } else { // Dependency not loaded yet, add to pending items pendingDefs.emplace_back(full, name); @@ -628,9 +670,10 @@ void ContentLoader::loadContent(const dv::value& root) { if (builder.items.get(parent)) { // Dependency resolved or parent exists in another pack, // load the item - auto& def = builder.items.create(it->first); + bool created; + auto& def = builder.items.create(it->first, &created); loadItem(def, it->first, it->second); - stats->totalItems++; + stats->totalItems += created; it = pendingDefs.erase(it); // Remove resolved item progressMade = true; } else { @@ -654,9 +697,10 @@ void ContentLoader::loadContent(const dv::value& root) { if (parent.empty() || builder.entities.get(parent)) { // No dependency or dependency already loaded/exists in another // content pack - auto& def = builder.entities.create(full); + bool created; + auto& def = builder.entities.create(full, &created); loadEntity(def, full, name); - stats->totalEntities++; + stats->totalEntities += created; } else { // Dependency not loaded yet, add to pending items pendingDefs.emplace_back(full, name); @@ -673,9 +717,10 @@ void ContentLoader::loadContent(const dv::value& root) { if (builder.entities.get(parent)) { // Dependency resolved or parent exists in another pack, // load the item - auto& def = builder.entities.create(it->first); + bool created; + auto& def = builder.entities.create(it->first, &created); loadEntity(def, it->first, it->second); - stats->totalEntities++; + stats->totalEntities += created; it = pendingDefs.erase(it); // Remove resolved item progressMade = true; } else { diff --git a/src/content/ContentPack.cpp b/src/content/ContentPack.cpp index 166400c6b..7135f2311 100644 --- a/src/content/ContentPack.cpp +++ b/src/content/ContentPack.cpp @@ -13,9 +13,9 @@ namespace fs = std::filesystem; -ContentPack ContentPack::createCore(const EnginePaths* paths) { +ContentPack ContentPack::createCore(const EnginePaths& paths) { return ContentPack { - "core", "Core", ENGINE_VERSION_STRING, "", "", paths->getResourcesFolder(), {} + "core", "Core", ENGINE_VERSION_STRING, "", "", paths.getResourcesFolder(), "res:", {} }; } @@ -70,7 +70,7 @@ static void checkContentPackId(const std::string& id, const fs::path& folder) { } } -ContentPack ContentPack::read(const fs::path& folder) { +ContentPack ContentPack::read(const std::string& path, const fs::path& folder) { auto root = files::read_json(folder / fs::path(PACKAGE_FILENAME)); ContentPack pack; root.at("id").get(pack.id); @@ -90,6 +90,7 @@ ContentPack ContentPack::read(const fs::path& folder) { root.at("description").get(pack.description); root.at("source").get(pack.source); pack.folder = folder; + pack.path = path; if (auto found = root.at("dependencies")) { const auto& dependencies = *found; @@ -123,17 +124,19 @@ ContentPack ContentPack::read(const fs::path& folder) { } void ContentPack::scanFolder( - const fs::path& folder, std::vector& packs + const std::string& path, const fs::path& folder, std::vector& packs ) { if (!fs::is_directory(folder)) { return; } for (const auto& entry : fs::directory_iterator(folder)) { - const fs::path& folder = entry.path(); - if (!fs::is_directory(folder)) continue; - if (!is_pack(folder)) continue; + const fs::path& packFolder = entry.path(); + if (!fs::is_directory(packFolder)) continue; + if (!is_pack(packFolder)) continue; try { - packs.push_back(read(folder)); + packs.push_back( + read(path + "/" + packFolder.filename().string(), packFolder) + ); } catch (const contentpack_error& err) { std::cerr << "package.json error at " << err.getFolder().u8string(); std::cerr << ": " << err.what() << std::endl; @@ -146,9 +149,7 @@ void ContentPack::scanFolder( std::vector ContentPack::worldPacksList(const fs::path& folder) { fs::path listfile = folder / fs::path("packs.list"); if (!fs::is_regular_file(listfile)) { - std::cerr << "warning: packs.list not found (will be created)"; - std::cerr << std::endl; - files::write_string(listfile, "# autogenerated, do not modify\nbase\n"); + throw std::runtime_error("missing file 'packs.list'"); } return files::read_list(listfile); } diff --git a/src/content/ContentPack.hpp b/src/content/ContentPack.hpp index 857f4fe0a..c8e0de9b0 100644 --- a/src/content/ContentPack.hpp +++ b/src/content/ContentPack.hpp @@ -10,18 +10,18 @@ class EnginePaths; -namespace fs = std::filesystem; - class contentpack_error : public std::runtime_error { std::string packId; - fs::path folder; + std::filesystem::path folder; public: contentpack_error( - std::string packId, fs::path folder, const std::string& message + std::string packId, + std::filesystem::path folder, + const std::string& message ); std::string getPackId() const; - fs::path getFolder() const; + std::filesystem::path getFolder() const; }; enum class DependencyLevel { @@ -42,45 +42,52 @@ struct ContentPack { std::string version = "0.0"; std::string creator = ""; std::string description = "no description"; - fs::path folder; + std::filesystem::path folder; + std::string path; std::vector dependencies; std::string source = ""; - fs::path getContentFile() const; + std::filesystem::path getContentFile() const; static inline const std::string PACKAGE_FILENAME = "package.json"; static inline const std::string CONTENT_FILENAME = "content.json"; - static inline const fs::path BLOCKS_FOLDER = "blocks"; - static inline const fs::path ITEMS_FOLDER = "items"; - static inline const fs::path ENTITIES_FOLDER = "entities"; - static inline const fs::path GENERATORS_FOLDER = "generators"; + static inline const std::filesystem::path BLOCKS_FOLDER = "blocks"; + static inline const std::filesystem::path ITEMS_FOLDER = "items"; + static inline const std::filesystem::path ENTITIES_FOLDER = "entities"; + static inline const std::filesystem::path GENERATORS_FOLDER = "generators"; static const std::vector RESERVED_NAMES; - static bool is_pack(const fs::path& folder); - static ContentPack read(const fs::path& folder); + static bool is_pack(const std::filesystem::path& folder); + static ContentPack read( + const std::string& path, const std::filesystem::path& folder + ); static void scanFolder( - const fs::path& folder, std::vector& packs + const std::string& path, + const std::filesystem::path& folder, + std::vector& packs ); - static std::vector worldPacksList(const fs::path& folder); + static std::vector worldPacksList( + const std::filesystem::path& folder + ); - static fs::path findPack( + static std::filesystem::path findPack( const EnginePaths* paths, - const fs::path& worldDir, + const std::filesystem::path& worldDir, const std::string& name ); - static ContentPack createCore(const EnginePaths*); + static ContentPack createCore(const EnginePaths&); - static inline fs::path getFolderFor(ContentType type) { + static inline std::filesystem::path getFolderFor(ContentType type) { switch (type) { case ContentType::BLOCK: return ContentPack::BLOCKS_FOLDER; case ContentType::ITEM: return ContentPack::ITEMS_FOLDER; case ContentType::ENTITY: return ContentPack::ENTITIES_FOLDER; case ContentType::GENERATOR: return ContentPack::GENERATORS_FOLDER; - case ContentType::NONE: return fs::u8path(""); - default: return fs::u8path(""); + case ContentType::NONE: return std::filesystem::u8path(""); + default: return std::filesystem::u8path(""); } } }; @@ -95,12 +102,13 @@ struct ContentPackStats { } }; -struct world_funcs_set { - bool onblockplaced : 1; - bool onblockreplaced : 1; - bool onblockbroken : 1; - bool onblockinteract : 1; - bool onplayertick : 1; +struct WorldFuncsSet { + bool onblockplaced; + bool onblockreplaced; + bool onblockbreaking; + bool onblockbroken; + bool onblockinteract; + bool onplayertick; }; class ContentPackRuntime { @@ -108,7 +116,7 @@ class ContentPackRuntime { ContentPackStats stats {}; scriptenv env; public: - world_funcs_set worldfuncsset {}; + WorldFuncsSet worldfuncsset {}; ContentPackRuntime(ContentPack info, scriptenv env); ~ContentPackRuntime(); diff --git a/src/content/PacksManager.cpp b/src/content/PacksManager.cpp index 370bed989..fdf39bb32 100644 --- a/src/content/PacksManager.cpp +++ b/src/content/PacksManager.cpp @@ -7,7 +7,7 @@ PacksManager::PacksManager() = default; -void PacksManager::setSources(std::vector sources) { +void PacksManager::setSources(std::vector> sources) { this->sources = std::move(sources); } @@ -15,8 +15,8 @@ void PacksManager::scan() { packs.clear(); std::vector packsList; - for (auto& folder : sources) { - ContentPack::scanFolder(folder, packsList); + for (auto& [path, folder] : sources) { + ContentPack::scanFolder(path, folder, packsList); for (auto& pack : packsList) { packs.try_emplace(pack.id, pack); } @@ -116,7 +116,7 @@ static bool resolve_dependencies( return satisfied; } -std::vector PacksManager::assembly( +std::vector PacksManager::assemble( const std::vector& names ) const { std::vector allNames = names; diff --git a/src/content/PacksManager.hpp b/src/content/PacksManager.hpp index 3784bbfbc..b60b06ce1 100644 --- a/src/content/PacksManager.hpp +++ b/src/content/PacksManager.hpp @@ -10,12 +10,12 @@ namespace fs = std::filesystem; class PacksManager { std::unordered_map packs; - std::vector sources; + std::vector> sources; public: PacksManager(); /// @brief Set content packs sources (search folders) - void setSources(std::vector sources); + void setSources(std::vector> sources); /// @brief Scan sources and collect all found packs excluding duplication. /// Scanning order depends on sources order @@ -38,7 +38,7 @@ class PacksManager { /// @return resulting ordered vector of pack names /// @throws contentpack_error if required dependency not found or /// circular dependency detected - std::vector assembly(const std::vector& names + std::vector assemble(const std::vector& names ) const; /// @brief Collect all pack names (identifiers) into a new vector diff --git a/src/core_defs.cpp b/src/core_defs.cpp index c62900f7f..fb8540baa 100644 --- a/src/core_defs.cpp +++ b/src/core_defs.cpp @@ -11,9 +11,9 @@ #include "voxels/Block.hpp" // All in-game definitions (blocks, items, etc..) -void corecontent::setup(EnginePaths* paths, ContentBuilder* builder) { +void corecontent::setup(const EnginePaths& paths, ContentBuilder& builder) { { - Block& block = builder->blocks.create(CORE_AIR); + Block& block = builder.blocks.create(CORE_AIR); block.replaceable = true; block.drawGroup = 1; block.lightPassing = true; @@ -24,11 +24,11 @@ void corecontent::setup(EnginePaths* paths, ContentBuilder* builder) { block.pickingItem = CORE_EMPTY; } { - ItemDef& item = builder->items.create(CORE_EMPTY); + ItemDef& item = builder.items.create(CORE_EMPTY); item.iconType = ItemIconType::NONE; } - auto bindsFile = paths->getResourcesFolder()/fs::path("bindings.toml"); + auto bindsFile = paths.getResourcesFolder()/fs::path("bindings.toml"); if (fs::is_regular_file(bindsFile)) { Events::loadBindings( bindsFile.u8string(), files::read_string(bindsFile), BindType::BIND @@ -36,20 +36,20 @@ void corecontent::setup(EnginePaths* paths, ContentBuilder* builder) { } { - Block& block = builder->blocks.create(CORE_OBSTACLE); + Block& block = builder.blocks.create(CORE_OBSTACLE); for (uint i = 0; i < 6; i++) { block.textureFaces[i] = "obstacle"; } block.hitboxes = {AABB()}; block.breakable = false; - ItemDef& item = builder->items.create(CORE_OBSTACLE+".item"); + ItemDef& item = builder.items.create(CORE_OBSTACLE+".item"); item.iconType = ItemIconType::BLOCK; item.icon = CORE_OBSTACLE; item.placingBlock = CORE_OBSTACLE; item.caption = block.caption; } { - Block& block = builder->blocks.create(CORE_STRUCT_AIR); + Block& block = builder.blocks.create(CORE_STRUCT_AIR); for (uint i = 0; i < 6; i++) { block.textureFaces[i] = "struct_air"; } @@ -58,7 +58,7 @@ void corecontent::setup(EnginePaths* paths, ContentBuilder* builder) { block.lightPassing = true; block.hitboxes = {AABB()}; block.obstacle = false; - ItemDef& item = builder->items.create(CORE_STRUCT_AIR+".item"); + ItemDef& item = builder.items.create(CORE_STRUCT_AIR+".item"); item.iconType = ItemIconType::BLOCK; item.icon = CORE_STRUCT_AIR; item.placingBlock = CORE_STRUCT_AIR; diff --git a/src/core_defs.hpp b/src/core_defs.hpp index 9f26b1c8a..8042ca362 100644 --- a/src/core_defs.hpp +++ b/src/core_defs.hpp @@ -21,12 +21,9 @@ inline const std::string BIND_MOVE_CROUCH = "movement.crouch"; inline const std::string BIND_MOVE_CHEAT = "movement.cheat"; inline const std::string BIND_CAM_ZOOM = "camera.zoom"; inline const std::string BIND_CAM_MODE = "camera.mode"; -inline const std::string BIND_PLAYER_NOCLIP = "player.noclip"; -inline const std::string BIND_PLAYER_FLIGHT = "player.flight"; inline const std::string BIND_PLAYER_ATTACK = "player.attack"; inline const std::string BIND_PLAYER_DESTROY = "player.destroy"; inline const std::string BIND_PLAYER_BUILD = "player.build"; -inline const std::string BIND_PLAYER_PICK = "player.pick"; inline const std::string BIND_PLAYER_FAST_INTERACTOIN = "player.fast_interaction"; inline const std::string BIND_HUD_INVENTORY = "hud.inventory"; @@ -35,5 +32,5 @@ class EnginePaths; class ContentBuilder; namespace corecontent { - void setup(EnginePaths* paths, ContentBuilder* builder); + void setup(const EnginePaths& paths, ContentBuilder& builder); } diff --git a/src/data/StructLayout.cpp b/src/data/StructLayout.cpp index a3e0fd233..3716bc44c 100644 --- a/src/data/StructLayout.cpp +++ b/src/data/StructLayout.cpp @@ -95,8 +95,8 @@ static inline FieldIncapatibilityType checkIncapatibility( static inline integer_t clamp_value(integer_t value, FieldType type) { auto typesize = sizeof_type(type) * CHAR_BIT; - integer_t minval = -(1 << (typesize-1)); - integer_t maxval = (1 << (typesize-1))-1; + integer_t minval = -(1LL << (typesize-1)); + integer_t maxval = (1LL << (typesize-1))-1; return std::min(maxval, std::max(minval, value)); } diff --git a/src/debug/Logger.cpp b/src/debug/Logger.cpp index e8f4c95b7..970b2102d 100644 --- a/src/debug/Logger.cpp +++ b/src/debug/Logger.cpp @@ -23,10 +23,16 @@ Logger::Logger(std::string name) : name(std::move(name)) { void Logger::log( LogLevel level, const std::string& name, const std::string& message ) { + if (level == LogLevel::print) { + std::cout << "[" << name << "] " << message << std::endl; + return; + } + using namespace std::chrono; std::stringstream ss; switch (level) { + case LogLevel::print: case LogLevel::debug: #ifdef NDEBUG return; diff --git a/src/debug/Logger.hpp b/src/debug/Logger.hpp index 6b931fe0a..9505affd4 100644 --- a/src/debug/Logger.hpp +++ b/src/debug/Logger.hpp @@ -5,7 +5,7 @@ #include namespace debug { - enum class LogLevel { debug, info, warning, error }; + enum class LogLevel { print, debug, info, warning, error }; class Logger; @@ -60,5 +60,10 @@ namespace debug { LogMessage warning() { return LogMessage(this, LogLevel::warning); } + + /// @brief Print-debugging tool (printed without header) + LogMessage print() { + return LogMessage(this, LogLevel::print); + } }; } diff --git a/src/delegates.hpp b/src/delegates.hpp index 324921643..0e670b413 100644 --- a/src/delegates.hpp +++ b/src/delegates.hpp @@ -8,6 +8,8 @@ using runnable = std::function; template using supplier = std::function; template using consumer = std::function; +using KeyCallback = std::function; + // data sources using wstringsupplier = std::function; using doublesupplier = std::function; diff --git a/src/devtools/syntax_highlighting.cpp b/src/devtools/syntax_highlighting.cpp index f68780df8..2963daf2e 100644 --- a/src/devtools/syntax_highlighting.cpp +++ b/src/devtools/syntax_highlighting.cpp @@ -32,7 +32,6 @@ static std::unique_ptr build_styles( continue; } if (token.start.pos > offset) { - int n = token.start.pos - offset; styles.map.insert(styles.map.end(), token.start.pos - offset, 0); } offset = token.end.pos; diff --git a/src/engine.cpp b/src/engine/Engine.cpp similarity index 58% rename from src/engine.cpp rename to src/engine/Engine.cpp index 129dc68db..9184fdd6d 100644 --- a/src/engine.cpp +++ b/src/engine/Engine.cpp @@ -1,6 +1,8 @@ -#include "engine.hpp" +#include "Engine.hpp" +#ifndef GLEW_STATIC #define GLEW_STATIC +#endif #include "debug/Logger.hpp" #include "assets/AssetsLoader.hpp" @@ -15,13 +17,10 @@ #include "content/ContentLoader.hpp" #include "core_defs.hpp" #include "files/files.hpp" -#include "files/settings_io.hpp" #include "frontend/locale.hpp" #include "frontend/menu.hpp" #include "frontend/screens/Screen.hpp" -#include "frontend/screens/MenuScreen.hpp" #include "graphics/render/ModelsGenerator.hpp" -#include "graphics/core/Batch2D.hpp" #include "graphics/core/DrawContext.hpp" #include "graphics/core/ImageData.hpp" #include "graphics/core/Shader.hpp" @@ -30,6 +29,7 @@ #include "logic/EngineController.hpp" #include "logic/CommandsInterpreter.hpp" #include "logic/scripting/scripting.hpp" +#include "logic/scripting/scripting_hud.hpp" #include "network/Network.hpp" #include "util/listutil.hpp" #include "util/platform.hpp" @@ -37,7 +37,9 @@ #include "window/Events.hpp" #include "window/input.hpp" #include "window/Window.hpp" -#include "settings.hpp" +#include "world/Level.hpp" +#include "Mainloop.hpp" +#include "ServerMainloop.hpp" #include #include @@ -71,52 +73,69 @@ static std::unique_ptr load_icon(const fs::path& resdir) { return nullptr; } -Engine::Engine(EngineSettings& settings, SettingsHandler& settingsHandler, EnginePaths* paths) - : settings(settings), settingsHandler(settingsHandler), paths(paths), +Engine::Engine(CoreParameters coreParameters) + : params(std::move(coreParameters)), + settings(), + settingsHandler({settings}), interpreter(std::make_unique()), - network(network::Network::create(settings.network)) -{ - paths->prepare(); + network(network::Network::create(settings.network)) { + logger.info() << "engine version: " << ENGINE_VERSION_STRING; + if (params.headless) { + logger.info() << "headless mode is enabled"; + } + paths.setResourcesFolder(params.resFolder); + paths.setUserFilesFolder(params.userFolder); + paths.prepare(); + if (!params.scriptFile.empty()) { + paths.setScriptFolder(params.scriptFile.parent_path()); + } loadSettings(); - auto resdir = paths->getResourcesFolder(); + auto resdir = paths.getResourcesFolder(); - controller = std::make_unique(this); - if (Window::initialize(&this->settings.display)){ - throw initialize_error("could not initialize window"); - } - if (auto icon = load_icon(resdir)) { - icon->flipY(); - Window::setIcon(icon.get()); + controller = std::make_unique(*this); + if (!params.headless) { + if (Window::initialize(&settings.display)){ + throw initialize_error("could not initialize window"); + } + time.set(Window::time()); + if (auto icon = load_icon(resdir)) { + icon->flipY(); + Window::setIcon(icon.get()); + } + loadControls(); + + gui = std::make_unique(); + if (ENGINE_DEBUG_BUILD) { + menus::create_version_label(*this); + } } - loadControls(); - audio::initialize(settings.audio.enabled.get()); + audio::initialize(settings.audio.enabled.get() && !params.headless); create_channel(this, "master", settings.audio.volumeMaster); create_channel(this, "regular", settings.audio.volumeRegular); create_channel(this, "music", settings.audio.volumeMusic); create_channel(this, "ambient", settings.audio.volumeAmbient); create_channel(this, "ui", settings.audio.volumeUI); - gui = std::make_unique(); - if (settings.ui.language.get() == "auto") { + bool langNotSet = settings.ui.language.get() == "auto"; + if (langNotSet) { settings.ui.language.set(langs::locale_by_envlocale( platform::detect_locale(), - paths->getResourcesFolder() + paths.getResourcesFolder() )); } - if (ENGINE_DEBUG_BUILD) { - menus::create_version_label(this); + scripting::initialize(this); + if (!isHeadless()) { + gui->setPageLoader(scripting::create_page_loader()); } - keepAlive(settings.ui.language.observe([=](auto lang) { + keepAlive(settings.ui.language.observe([this](auto lang) { setLanguage(lang); }, true)); - - scripting::initialize(this); basePacks = files::read_list(resdir/fs::path("config/builtins.list")); } void Engine::loadSettings() { - fs::path settings_file = paths->getSettingsFile(); + fs::path settings_file = paths.getSettingsFile(); if (fs::is_regular_file(settings_file)) { logger.info() << "loading settings"; std::string text = files::read_string(settings_file); @@ -130,7 +149,7 @@ void Engine::loadSettings() { } void Engine::loadControls() { - fs::path controls_file = paths->getControlsFile(); + fs::path controls_file = paths.getControlsFile(); if (fs::is_regular_file(controls_file)) { logger.info() << "loading controls"; std::string text = files::read_string(controls_file); @@ -143,13 +162,6 @@ void Engine::onAssetsLoaded() { gui->onAssetsLoad(assets.get()); } -void Engine::updateTimers() { - frame++; - double currentTime = Window::time(); - delta = currentTime - lastTime; - lastTime = currentTime; -} - void Engine::updateHotkeys() { if (Events::jpressed(keycode::F2)) { saveScreenshot(); @@ -162,67 +174,58 @@ void Engine::updateHotkeys() { void Engine::saveScreenshot() { auto image = Window::takeScreenshot(); image->flipY(); - fs::path filename = paths->getNewScreenshotFile("png"); + fs::path filename = paths.getNewScreenshotFile("png"); imageio::write(filename.string(), image.get()); logger.info() << "saved screenshot as " << filename.u8string(); } -void Engine::mainloop() { - logger.info() << "starting menu screen"; - setScreen(std::make_shared(this)); - - Batch2D batch(1024); - lastTime = Window::time(); - - logger.info() << "engine started"; - while (!Window::isShouldClose()){ - assert(screen != nullptr); - updateTimers(); - updateHotkeys(); - audio::update(delta); - - gui->act(delta, Viewport(Window::width, Window::height)); - screen->update(delta); +void Engine::run() { + if (params.headless) { + ServerMainloop(*this).run(); + } else { + Mainloop(*this).run(); + } +} - if (!Window::isIconified()) { - renderFrame(batch); - } - Window::setFramerate( - Window::isIconified() && settings.display.limitFpsIconified.get() - ? 20 - : settings.display.framerate.get() - ); +void Engine::postUpdate() { + network->update(); + postRunnables.run(); + scripting::process_post_runnables(); +} - network->update(); - processPostRunnables(); +void Engine::updateFrontend() { + double delta = time.getDelta(); + updateHotkeys(); + audio::update(delta); + gui->act(delta, Viewport(Window::width, Window::height)); + screen->update(delta); +} - Window::swapBuffers(); - Events::pollEvents(); - } +void Engine::nextFrame() { + Window::setFramerate( + Window::isIconified() && settings.display.limitFpsIconified.get() + ? 20 + : settings.display.framerate.get() + ); + Window::swapBuffers(); + Events::pollEvents(); } -void Engine::renderFrame(Batch2D& batch) { - screen->draw(delta); +void Engine::renderFrame() { + screen->draw(time.getDelta()); Viewport viewport(Window::width, Window::height); - DrawContext ctx(nullptr, viewport, &batch); + DrawContext ctx(nullptr, viewport, nullptr); gui->draw(ctx, *assets); } -void Engine::processPostRunnables() { - std::lock_guard lock(postRunnablesMutex); - while (!postRunnables.empty()) { - postRunnables.front()(); - postRunnables.pop(); - } - scripting::process_post_runnables(); -} - void Engine::saveSettings() { logger.info() << "saving settings"; - files::write_string(paths->getSettingsFile(), toml::stringify(settingsHandler)); - logger.info() << "saving bindings"; - files::write_string(paths->getControlsFile(), Events::writeBindings()); + files::write_string(paths.getSettingsFile(), toml::stringify(settingsHandler)); + if (!params.headless) { + logger.info() << "saving bindings"; + files::write_string(paths.getControlsFile(), Events::writeBindings()); + } } Engine::~Engine() { @@ -235,13 +238,19 @@ Engine::~Engine() { content.reset(); assets.reset(); interpreter.reset(); - gui.reset(); - logger.info() << "gui finished"; + if (gui) { + gui.reset(); + logger.info() << "gui finished"; + } audio::close(); network.reset(); + clearKeepedObjects(); scripting::close(); logger.info() << "scripting finished"; - Window::terminate(); + if (!params.headless) { + Window::terminate(); + logger.info() << "window closed"; + } logger.info() << "engine finished"; } @@ -256,13 +265,17 @@ cmd::CommandsInterpreter* Engine::getCommandsInterpreter() { PacksManager Engine::createPacksManager(const fs::path& worldFolder) { PacksManager manager; manager.setSources({ - worldFolder/fs::path("content"), - paths->getUserFilesFolder()/fs::path("content"), - paths->getResourcesFolder()/fs::path("content") + {"world:content", worldFolder.empty() ? worldFolder : worldFolder/fs::path("content")}, + {"user:content", paths.getUserFilesFolder()/fs::path("content")}, + {"res:content", paths.getResourcesFolder()/fs::path("content")} }); return manager; } +void Engine::setLevelConsumer(consumer> levelConsumer) { + this->levelConsumer = std::move(levelConsumer); +} + void Engine::loadAssets() { logger.info() << "loading assets"; Shader::preprocessor->setPaths(resPaths.get()); @@ -278,42 +291,36 @@ void Engine::loadAssets() { auto task = loader.startTask([=](){}); task->waitForEnd(); } else { - try { - while (loader.hasNext()) { - loader.loadNext(); - } - } catch (const assetload::error& err) { - new_assets.reset(); - throw; + while (loader.hasNext()) { + loader.loadNext(); } } assets = std::move(new_assets); - - if (content) { - for (auto& [name, def] : content->blocks.getDefs()) { - if (def->model == BlockModel::custom) { - if (def->modelName.empty()) { - assets->store( - std::make_unique( - ModelsGenerator::loadCustomBlockModel( - def->customModelRaw, *assets, !def->shadeless - ) - ), - name + ".model" - ); - def->modelName = def->name + ".model"; - } - } - } - for (auto& [name, def] : content->items.getDefs()) { + + if (content == nullptr) { + return; + } + for (auto& [name, def] : content->blocks.getDefs()) { + if (def->model == BlockModel::custom && def->modelName.empty()) { assets->store( std::make_unique( - ModelsGenerator::generate(*def, *content, *assets) + ModelsGenerator::loadCustomBlockModel( + def->customModelRaw, *assets, !def->shadeless + ) ), name + ".model" ); + def->modelName = def->name + ".model"; } } + for (auto& [name, def] : content->items.getDefs()) { + assets->store( + std::make_unique( + ModelsGenerator::generate(*def, *content, *assets) + ), + name + ".model" + ); + } } static void load_configs(const fs::path& root) { @@ -329,7 +336,7 @@ static void load_configs(const fs::path& root) { void Engine::loadContent() { scripting::cleanup(); - auto resdir = paths->getResourcesFolder(); + auto resdir = paths.getResourcesFolder(); std::vector names; for (auto& pack : contentPacks) { @@ -337,12 +344,12 @@ void Engine::loadContent() { } ContentBuilder contentBuilder; - corecontent::setup(paths, &contentBuilder); + corecontent::setup(paths, contentBuilder); - paths->setContentPacks(&contentPacks); - PacksManager manager = createPacksManager(paths->getCurrentWorldFolder()); + paths.setContentPacks(&contentPacks); + PacksManager manager = createPacksManager(paths.getCurrentWorldFolder()); manager.scan(); - names = manager.assembly(names); + names = manager.assemble(names); contentPacks = manager.getAll(names); auto corePack = ContentPack::createCore(paths); @@ -372,13 +379,15 @@ void Engine::loadContent() { ContentLoader::loadScripts(*content); langs::setup(resdir, langs::current->getId(), contentPacks); - loadAssets(); - onAssetsLoaded(); + if (!isHeadless()) { + loadAssets(); + onAssetsLoaded(); + } } void Engine::resetContent() { scripting::cleanup(); - auto resdir = paths->getResourcesFolder(); + auto resdir = paths.getResourcesFolder(); std::vector resRoots; { auto pack = ContentPack::createCore(paths); @@ -395,8 +404,10 @@ void Engine::resetContent() { content.reset(); langs::setup(resdir, langs::current->getId(), contentPacks); - loadAssets(); - onAssetsLoaded(); + if (!isHeadless()) { + loadAssets(); + onAssetsLoaded(); + } contentPacks = manager.getAll(basePacks); } @@ -405,26 +416,23 @@ void Engine::loadWorldContent(const fs::path& folder) { contentPacks.clear(); auto packNames = ContentPack::worldPacksList(folder); PacksManager manager; - manager.setSources({ - folder/fs::path("content"), - paths->getUserFilesFolder()/fs::path("content"), - paths->getResourcesFolder()/fs::path("content") - }); + manager.setSources( + {{"world:content", + folder.empty() ? folder : folder / fs::path("content")}, + {"user:content", paths.getUserFilesFolder() / fs::path("content")}, + {"res:content", paths.getResourcesFolder() / fs::path("content")}} + ); manager.scan(); - contentPacks = manager.getAll(manager.assembly(packNames)); - paths->setCurrentWorldFolder(folder); + contentPacks = manager.getAll(manager.assemble(packNames)); + paths.setCurrentWorldFolder(folder); loadContent(); } void Engine::loadAllPacks() { - PacksManager manager = createPacksManager(paths->getCurrentWorldFolder()); + PacksManager manager = createPacksManager(paths.getCurrentWorldFolder()); manager.scan(); auto allnames = manager.getAllNames(); - contentPacks = manager.getAll(manager.assembly(allnames)); -} - -double Engine::getDelta() const { - return delta; + contentPacks = manager.getAll(manager.assemble(allnames)); } void Engine::setScreen(std::shared_ptr screen) { @@ -435,8 +443,28 @@ void Engine::setScreen(std::shared_ptr screen) { } void Engine::setLanguage(std::string locale) { - langs::setup(paths->getResourcesFolder(), std::move(locale), contentPacks); - gui->getMenu()->setPageLoader(menus::create_page_loader(this)); + langs::setup(paths.getResourcesFolder(), std::move(locale), contentPacks); +} + +void Engine::onWorldOpen(std::unique_ptr level) { + logger.info() << "world open"; + levelConsumer(std::move(level)); +} + +void Engine::onWorldClosed() { + logger.info() << "world closed"; + levelConsumer(nullptr); +} + +void Engine::quit() { + quitSignal = true; + if (!isHeadless()) { + Window::setShouldClose(true); + } +} + +bool Engine::isQuitSignal() const { + return quitSignal; } gui::GUI* Engine::getGUI() { @@ -469,7 +497,7 @@ std::vector& Engine::getBasePacks() { return basePacks; } -EnginePaths* Engine::getPaths() { +EnginePaths& Engine::getPaths() { return paths; } @@ -481,11 +509,6 @@ std::shared_ptr Engine::getScreen() { return screen; } -void Engine::postRunnable(const runnable& callback) { - std::lock_guard lock(postRunnablesMutex); - postRunnables.push(callback); -} - SettingsHandler& Engine::getSettingsHandler() { return settingsHandler; } @@ -493,3 +516,15 @@ SettingsHandler& Engine::getSettingsHandler() { network::Network& Engine::getNetwork() { return *network; } + +Time& Engine::getTime() { + return time; +} + +const CoreParameters& Engine::getCoreParameters() const { + return params; +} + +bool Engine::isHeadless() const { + return params.headless; +} diff --git a/src/engine.hpp b/src/engine/Engine.hpp similarity index 70% rename from src/engine.hpp rename to src/engine/Engine.hpp index 04a221dff..af9f7fb9f 100644 --- a/src/engine.hpp +++ b/src/engine/Engine.hpp @@ -2,32 +2,32 @@ #include "delegates.hpp" #include "typedefs.hpp" +#include "settings.hpp" #include "assets/Assets.hpp" #include "content/content_fwd.hpp" #include "content/ContentPack.hpp" #include "content/PacksManager.hpp" #include "files/engine_paths.hpp" +#include "files/settings_io.hpp" #include "util/ObjectsKeeper.hpp" +#include "PostRunnables.hpp" +#include "Time.hpp" #include #include -#include #include #include #include -#include +class Level; class Screen; class EnginePaths; class ResPaths; -class Batch2D; class EngineController; class SettingsHandler; struct EngineSettings; -namespace fs = std::filesystem; - namespace gui { class GUI; } @@ -45,44 +45,52 @@ class initialize_error : public std::runtime_error { initialize_error(const std::string& message) : std::runtime_error(message) {} }; +struct CoreParameters { + bool headless = false; + bool testMode = false; + std::filesystem::path resFolder {"res"}; + std::filesystem::path userFolder {"."}; + std::filesystem::path scriptFile; +}; + class Engine : public util::ObjectsKeeper { - EngineSettings& settings; - SettingsHandler& settingsHandler; - EnginePaths* paths; + CoreParameters params; + EngineSettings settings; + SettingsHandler settingsHandler; + EnginePaths paths; std::unique_ptr assets; std::shared_ptr screen; std::vector contentPacks; std::unique_ptr content; std::unique_ptr resPaths; - std::queue postRunnables; - std::recursive_mutex postRunnablesMutex; std::unique_ptr controller; std::unique_ptr interpreter; std::unique_ptr network; std::vector basePacks; - - uint64_t frame = 0; - double lastTime = 0.0; - double delta = 0.0; - std::unique_ptr gui; + PostRunnables postRunnables; + Time time; + consumer> levelConsumer; + bool quitSignal = false; void loadControls(); void loadSettings(); void saveSettings(); - void updateTimers(); void updateHotkeys(); - void renderFrame(Batch2D& batch); - void processPostRunnables(); void loadAssets(); public: - Engine(EngineSettings& settings, SettingsHandler& settingsHandler, EnginePaths* paths); + Engine(CoreParameters coreParameters); ~Engine(); - - /// @brief Start main engine input/update/render loop. - /// Automatically sets MenuScreen - void mainloop(); + + /// @brief Start the engine + void run(); + + void postUpdate(); + + void updateFrontend(); + void renderFrame(); + void nextFrame(); /// @brief Called after assets loading when all engine systems are initialized void onAssetsLoaded(); @@ -100,6 +108,7 @@ class Engine : public util::ObjectsKeeper { /// @brief Load all selected content-packs and reload assets void loadContent(); + /// @brief Reset content to base packs list void resetContent(); /// @brief Collect world content-packs and load content @@ -110,9 +119,6 @@ class Engine : public util::ObjectsKeeper { /// @brief Collect all available content-packs from res/content void loadAllPacks(); - /// @brief Get current frame delta-time - double getDelta() const; - /// @brief Get active assets storage instance Assets* getAssets(); @@ -123,11 +129,18 @@ class Engine : public util::ObjectsKeeper { EngineSettings& getSettings(); /// @brief Get engine filesystem paths source - EnginePaths* getPaths(); + EnginePaths& getPaths(); /// @brief Get engine resource paths controller ResPaths* getResPaths(); + void onWorldOpen(std::unique_ptr level); + void onWorldClosed(); + + void quit(); + + bool isQuitSignal() const; + /// @brief Get current Content instance const Content* getContent() const; @@ -142,7 +155,9 @@ class Engine : public util::ObjectsKeeper { std::shared_ptr getScreen(); /// @brief Enqueue function call to the end of current frame in draw thread - void postRunnable(const runnable& callback); + void postRunnable(const runnable& callback) { + postRunnables.postRunnable(callback); + } void saveScreenshot(); @@ -151,7 +166,15 @@ class Engine : public util::ObjectsKeeper { PacksManager createPacksManager(const fs::path& worldFolder); + void setLevelConsumer(consumer> levelConsumer); + SettingsHandler& getSettingsHandler(); network::Network& getNetwork(); + + Time& getTime(); + + const CoreParameters& getCoreParameters() const; + + bool isHeadless() const; }; diff --git a/src/engine/Mainloop.cpp b/src/engine/Mainloop.cpp new file mode 100644 index 000000000..e0e64d10c --- /dev/null +++ b/src/engine/Mainloop.cpp @@ -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(engine)); + } else { + engine.setScreen(std::make_shared(engine, std::move(level))); + } + }); + + logger.info() << "starting menu screen"; + engine.setScreen(std::make_shared(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"; +} diff --git a/src/engine/Mainloop.hpp b/src/engine/Mainloop.hpp new file mode 100644 index 000000000..3cd2f608f --- /dev/null +++ b/src/engine/Mainloop.hpp @@ -0,0 +1,11 @@ +#pragma once + +class Engine; + +class Mainloop { + Engine& engine; +public: + Mainloop(Engine& engine); + + void run(); +}; diff --git a/src/engine/PostRunnables.hpp b/src/engine/PostRunnables.hpp new file mode 100644 index 000000000..dc227c889 --- /dev/null +++ b/src/engine/PostRunnables.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include +#include +#include "delegates.hpp" + +class PostRunnables { + std::queue runnables; + std::recursive_mutex mutex; +public: + void postRunnable(runnable task) { + std::lock_guard lock(mutex); + runnables.push(std::move(task)); + } + + void run() { + std::queue tasksToRun; + { + std::lock_guard lock(mutex); + std::swap(tasksToRun, runnables); + } + + while (!tasksToRun.empty()) { + auto& task = tasksToRun.front(); + task(); + tasksToRun.pop(); + } + } +}; diff --git a/src/engine/ServerMainloop.cpp b/src/engine/ServerMainloop.cpp new file mode 100644 index 000000000..eb570e7bf --- /dev/null +++ b/src/engine/ServerMainloop.cpp @@ -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 + +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(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(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(end - begin).count() / 1000); + begin = system_clock::now(); + } + } + logger.info() << "test finished"; +} + +void ServerMainloop::setLevel(std::unique_ptr level) { + if (level == nullptr) { + controller->onWorldQuit(); + engine.getPaths().setCurrentWorldFolder(fs::path()); + controller = nullptr; + } else { + controller = std::make_unique( + &engine, std::move(level), nullptr + ); + } +} diff --git a/src/engine/ServerMainloop.hpp b/src/engine/ServerMainloop.hpp new file mode 100644 index 000000000..28cfbd9de --- /dev/null +++ b/src/engine/ServerMainloop.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include + +class Level; +class LevelController; +class Engine; + +class ServerMainloop { + Engine& engine; + std::unique_ptr controller; +public: + ServerMainloop(Engine& engine); + ~ServerMainloop(); + + void run(); + + void setLevel(std::unique_ptr level); +}; diff --git a/src/engine/Time.hpp b/src/engine/Time.hpp new file mode 100644 index 000000000..9f914b1fc --- /dev/null +++ b/src/engine/Time.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include + +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; + } +}; diff --git a/src/files/WorldFiles.cpp b/src/files/WorldFiles.cpp index c99ee8ef0..3759c96b1 100644 --- a/src/files/WorldFiles.cpp +++ b/src/files/WorldFiles.cpp @@ -159,9 +159,9 @@ std::optional WorldFiles::readWorldInfo() { } static void read_resources_data( - const Content* content, const dv::value& list, ResourceType type + const Content& content, const dv::value& list, ResourceType type ) { - const auto& indices = content->getIndices(type); + const auto& indices = content.getIndices(type); for (size_t i = 0; i < list.size(); i++) { auto& map = list[i]; const auto& name = map["name"].asString(); @@ -174,7 +174,7 @@ static void read_resources_data( } } -bool WorldFiles::readResourcesData(const Content* content) { +bool WorldFiles::readResourcesData(const Content& content) { fs::path file = getResourcesFile(); if (!fs::is_regular_file(file)) { logger.warning() << "resources.json does not exists"; diff --git a/src/files/WorldFiles.hpp b/src/files/WorldFiles.hpp index 7f72e1463..2d8ff0f0a 100644 --- a/src/files/WorldFiles.hpp +++ b/src/files/WorldFiles.hpp @@ -49,7 +49,7 @@ class WorldFiles { void createDirectories(); std::optional readWorldInfo(); - bool readResourcesData(const Content* content); + bool readResourcesData(const Content& content); static void createContentIndicesCache( const ContentIndices* indices, dv::value& root diff --git a/src/files/engine_paths.cpp b/src/files/engine_paths.cpp index d92518970..2c8cd02cc 100644 --- a/src/files/engine_paths.cpp +++ b/src/files/engine_paths.cpp @@ -48,6 +48,18 @@ static std::filesystem::path toCanonic(std::filesystem::path path) { } void EnginePaths::prepare() { + if (!fs::is_directory(resourcesFolder)) { + throw std::runtime_error( + resourcesFolder.u8string() + " is not a directory" + ); + } + if (!fs::is_directory(userFilesFolder)) { + fs::create_directories(userFilesFolder); + } + + logger.info() << "resources folder: " << fs::canonical(resourcesFolder).u8string(); + logger.info() << "user files folder: " << fs::canonical(userFilesFolder).u8string(); + auto contentFolder = userFilesFolder / CONTENT_FOLDER; if (!fs::is_directory(contentFolder)) { fs::create_directories(contentFolder); @@ -120,7 +132,7 @@ std::filesystem::path EnginePaths::getSettingsFile() const { return userFilesFolder / SETTINGS_FILE; } -std::vector EnginePaths::scanForWorlds() { +std::vector EnginePaths::scanForWorlds() const { std::vector folders; auto folder = getWorldsFolder(); @@ -157,6 +169,10 @@ void EnginePaths::setResourcesFolder(std::filesystem::path folder) { this->resourcesFolder = std::move(folder); } +void EnginePaths::setScriptFolder(std::filesystem::path folder) { + this->scriptFolder = std::move(folder); +} + void EnginePaths::setCurrentWorldFolder(std::filesystem::path folder) { this->currentWorldFolder = std::move(folder); } @@ -177,7 +193,7 @@ std::tuple EnginePaths::parsePath(std::string_view pat std::filesystem::path EnginePaths::resolve( const std::string& path, bool throwErr -) { +) const { auto [prefix, filename] = EnginePaths::parsePath(path); if (prefix.empty()) { throw files_access_error("no entry point specified"); @@ -199,7 +215,9 @@ std::filesystem::path EnginePaths::resolve( if (prefix == "export") { return userFilesFolder / EXPORT_FOLDER / fs::u8path(filename); } - + if (prefix == "script" && scriptFolder) { + return scriptFolder.value() / fs::u8path(filename); + } if (contentPacks) { for (auto& pack : *contentPacks) { if (pack.id == prefix) { diff --git a/src/files/engine_paths.hpp b/src/files/engine_paths.hpp index c52892437..5ec347046 100644 --- a/src/files/engine_paths.hpp +++ b/src/files/engine_paths.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -26,6 +27,8 @@ class EnginePaths { void setResourcesFolder(std::filesystem::path folder); std::filesystem::path getResourcesFolder() const; + void setScriptFolder(std::filesystem::path folder); + std::filesystem::path getWorldFolderByName(const std::string& name); std::filesystem::path getWorldsFolder() const; std::filesystem::path getConfigFolder() const; @@ -39,9 +42,9 @@ class EnginePaths { void setContentPacks(std::vector* contentPacks); - std::vector scanForWorlds(); + std::vector 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 parsePath(std::string_view view); @@ -51,6 +54,7 @@ class EnginePaths { std::filesystem::path userFilesFolder {"."}; std::filesystem::path resourcesFolder {"res"}; std::filesystem::path currentWorldFolder; + std::optional scriptFolder; std::vector* contentPacks = nullptr; }; diff --git a/src/files/settings_io.cpp b/src/files/settings_io.cpp index fe251a11e..f5f1341b0 100644 --- a/src/files/settings_io.cpp +++ b/src/files/settings_io.cpp @@ -73,6 +73,7 @@ SettingsHandler::SettingsHandler(EngineSettings& settings) { builder.add("frustum-culling", &settings.graphics.frustumCulling); builder.add("skybox-resolution", &settings.graphics.skyboxResolution); builder.add("chunk-max-vertices", &settings.graphics.chunkMaxVertices); + builder.add("chunk-max-vertices-dense", &settings.graphics.chunkMaxVerticesDense); builder.add("chunk-max-renderers", &settings.graphics.chunkMaxRenderers); builder.section("ui"); diff --git a/src/frontend/LevelFrontend.cpp b/src/frontend/LevelFrontend.cpp index d3cbb9e8d..1bc6e9e23 100644 --- a/src/frontend/LevelFrontend.cpp +++ b/src/frontend/LevelFrontend.cpp @@ -23,18 +23,18 @@ LevelFrontend::LevelFrontend( controller(controller), assets(assets), contentCache(std::make_unique( - *level.content, assets, settings.graphics + level.content, assets, settings.graphics )) { assets.store( BlocksPreview::build( - *contentCache, assets, *level.content->getIndices() + *contentCache, assets, *level.content.getIndices() ), "block-previews" ); controller->getBlocksController()->listenBlockInteraction( [currentPlayer, controller, &assets](auto player, const auto& pos, const auto& def, BlockInteraction type) { const auto& level = *controller->getLevel(); - auto material = level.content->findBlockMaterial(def.material); + auto material = level.content.findBlockMaterial(def.material); if (material == nullptr) { return; } diff --git a/src/frontend/debug_panel.cpp b/src/frontend/debug_panel.cpp index 9043f5b9d..a234c38aa 100644 --- a/src/frontend/debug_panel.cpp +++ b/src/frontend/debug_panel.cpp @@ -1,6 +1,6 @@ #include "audio/audio.hpp" #include "delegates.hpp" -#include "engine.hpp" +#include "engine/Engine.hpp" #include "settings.hpp" #include "hud.hpp" #include "content/Content.hpp" @@ -21,6 +21,7 @@ #include "voxels/Block.hpp" #include "voxels/Chunk.hpp" #include "voxels/Chunks.hpp" +#include "voxels/GlobalChunks.hpp" #include "world/Level.hpp" #include "world/World.hpp" @@ -41,7 +42,7 @@ static std::shared_ptr