diff --git a/.github/workflows/appimage.yml b/.github/workflows/appimage.yml index 250fd001a..25ae572fc 100644 --- a/.github/workflows/appimage.yml +++ b/.github/workflows/appimage.yml @@ -1,10 +1,10 @@ -name: C/C++ AppImage +name: x86-64 AppImage on: push: branches: [ "main", "release-**"] pull_request: - branches: [ "main" ] + branches: [ "main", "headless-mode" ] jobs: build-appimage: @@ -20,11 +20,11 @@ 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 # 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 @@ -34,10 +34,18 @@ jobs: cmake -DCMAKE_BUILD_TYPE=Release .. 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=Release -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 --output-always - name: Build AppImage uses: AppImageCrafters/build-appimage-action@fe2205a4d6056be47051f7b1b3811106e9814910 env: diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index e4ddf55ab..24832af93 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -4,7 +4,7 @@ on: push: branches: [ "main", "release-**"] pull_request: - branches: [ "main" ] + branches: [ "main", "headless-mode" ] jobs: build-dmg: @@ -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 --output-always - 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..183f2d6dc --- /dev/null +++ b/.github/workflows/windows-clang.yml @@ -0,0 +1,75 @@ +name: Windows Build (CLang) + +on: + push: + branches: [ "main", "release-**"] + pull_request: + branches: [ "main", "headless-mode" ] + +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 + mkdir build + cd build + cmake -G "MinGW Makefiles" -DVCPKG_TARGET_TRIPLET=x64-mingw-static -DCMAKE_BUILD_TYPE=Release -DVOXELENGINE_BUILD_WINDOWS_VCPKG=ON .. + 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 + - env: + MSYS2_LOCATION: ${{ steps.msys2.outputs.msys2-location }} + name: Add lua51.dll to the package + run: | + cp $env:MSYS2_LOCATION/clang64/bin/lua51.dll ${{ github.workspace }}/packaged/ + working-directory: ${{ github.workspace }} + - 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 --output-always diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index b14c32d0c..d417197c9 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -1,10 +1,10 @@ -name: Windows Build +name: MSVC Build on: push: branches: [ "main", "release-**"] pull_request: - branches: [ "main" ] + branches: [ "main", "headless-mode" ] jobs: build-windows: @@ -34,6 +34,12 @@ jobs: cd build cmake -DCMAKE_BUILD_TYPE=Release -DVOXELENGINE_BUILD_WINDOWS_VCPKG=ON -DVOXELENGINE_BUILD_TESTS=ON .. cmake --build . --config Release + - name: Run tests + run: ctest --output-on-failure --test-dir build + - name: Run engine tests + run: | + build/vctest/Release/vctest.exe -e build/Release/VoxelEngine.exe -d dev/tests -u build --output-always + timeout-minutes: 1 - name: Package for Windows run: | mkdir packaged @@ -41,9 +47,7 @@ jobs: cp C:/Windows/System32/msvcp140.dll packaged/Release/msvcp140.dll mv packaged/Release/VoxelEngine.exe packaged/Release/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: 'build/Release/*' diff --git a/CMakeLists.txt b/CMakeLists.txt index a56457bd2..a2415ad97 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,7 +12,7 @@ option(VOXELENGINE_BUILD_TESTS OFF) set(CMAKE_CXX_STANDARD 17) 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) @@ -39,6 +39,9 @@ else() if (CMAKE_BUILD_TYPE MATCHES "Debug") target_compile_options(${PROJECT_NAME} PRIVATE -Og) endif() + if (WIN32) + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static-libstdc++") + endif() endif() if(VOXELENGINE_BUILD_WINDOWS_VCPKG AND WIN32) @@ -81,4 +84,6 @@ file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/res DESTINATION ${CMAKE_CURRENT_BINARY_DIR if (VOXELENGINE_BUILD_TESTS) enable_testing() add_subdirectory(test) -endif() \ No newline at end of file +endif() + +add_subdirectory(vctest) diff --git a/dev/tests/example.lua b/dev/tests/example.lua new file mode 100644 index 000000000..c066c1ed4 --- /dev/null +++ b/dev/tests/example.lua @@ -0,0 +1,19 @@ +test.set_setting("chunks.load-distance", 3) +test.set_setting("chunks.load-speed", 16) + +test.reconfig_packs({"base"}, {}) +test.new_world("demo", "2019", "core:default") +local pid = player.create("Xerxes") +assert(player.get_name(pid) == "Xerxes") +test.sleep_until(function() return world.count_chunks() >= 9 end, 1000) +print(world.count_chunks()) + +for i=1,3 do + print("---") + timeit(1000000, block.get, 0, 0, 0) + timeit(1000000, block.get_slow, 0, 0, 0) +end + +block.destruct(0, 0, 0, pid) +assert(block.get(0, 0, 0) == 0) +test.close_world(true) diff --git a/dev/tests/world.lua b/dev/tests/world.lua new file mode 100644 index 000000000..3e7353e39 --- /dev/null +++ b/dev/tests/world.lua @@ -0,0 +1,23 @@ +-- Create/close/open/close world + +-- Open +test.new_world("demo", "2019", "core:default") +assert(world.is_open()) +assert(world.get_generator() == "core:default") +test.sleep(1) +assert(world.get_total_time() > 0.0) +print(world.get_total_time()) + +-- Close +test.close_world(true) +assert(not world.is_open()) + +-- Reopen +test.open_world("demo") +assert(world.is_open()) +assert(world.get_total_time() > 0.0) +assert(world.get_seed() == 2019) +test.tick() + +-- Close +test.close_world(true) 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/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/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/scripts/world.lua b/res/content/base/scripts/world.lua index 62d111ae3..3f69dc444 100644 --- a/res/content/base/scripts/world.lua +++ b/res/content/base/scripts/world.lua @@ -1,12 +1,14 @@ 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 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..0298a2b9b 100644 --- a/res/scripts/stdlib.lua +++ b/res/scripts/stdlib.lua @@ -9,6 +9,28 @@ function sleep(timesec) end end +if test then + test.sleep = sleep + test.name = __VC_TEST_NAME + test.new_world = core.new_world + test.open_world = core.open_world + test.close_world = core.close_world + test.reconfig_packs = core.reconfig_packs + test.set_setting = core.set_setting + test.tick = coroutine.yield + + function test.sleep_until(predicate, max_ticks) + max_ticks = max_ticks or 1e9 + local ticks = 0 + while ticks < max_ticks and not predicate() do + test.tick() + end + if ticks == max_ticks then + error("max ticks exceed") + end + end +end + ------------------------------------------------ ------------------- Events --------------------- ------------------------------------------------ @@ -328,6 +350,43 @@ function __vc_on_world_quit() _rules.clear() end +local __vc_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 + 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 + coroutine.close(co) + __vc_coroutines[id] = nil + end +end + assets = {} assets.load_texture = core.__load_texture diff --git a/res/scripts/stdmin.lua b/res/scripts/stdmin.lua index 13934e066..71fdbda2d 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 ---------------------------------------------- diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7eecaef99..4e8ae3489 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -4,7 +4,7 @@ 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}) @@ -20,10 +20,19 @@ if (NOT APPLE) find_package(EnTT REQUIRED) endif() +set(LIBS "") + 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") + if (MSVC) + 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") + else() + find_package(PkgConfig) + pkg_check_modules(OpenAL REQUIRED IMPORTED_TARGET openal) + set(LIBS ${LIBS} luajit-5.1 wsock32 ws2_32 pthread PkgConfig::OpenAL -static-libstdc++) + message(${OPENAL_LIBRARY}) + endif() find_package(glfw3 REQUIRED) find_package(glm REQUIRED) find_package(vorbis REQUIRED) @@ -53,8 +62,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/Mainloop.cpp b/src/Mainloop.cpp new file mode 100644 index 000000000..eae4c501c --- /dev/null +++ b/src/Mainloop.cpp @@ -0,0 +1,43 @@ +#include "Mainloop.hpp" + +#include "debug/Logger.hpp" +#include "engine.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/Mainloop.hpp b/src/Mainloop.hpp new file mode 100644 index 000000000..3cd2f608f --- /dev/null +++ b/src/Mainloop.hpp @@ -0,0 +1,11 @@ +#pragma once + +class Engine; + +class Mainloop { + Engine& engine; +public: + Mainloop(Engine& engine); + + void run(); +}; diff --git a/src/TestMainloop.cpp b/src/TestMainloop.cpp new file mode 100644 index 000000000..334870591 --- /dev/null +++ b/src/TestMainloop.cpp @@ -0,0 +1,54 @@ +#include "TestMainloop.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 "engine.hpp" + +static debug::Logger logger("mainloop"); + +inline constexpr int TPS = 20; + +TestMainloop::TestMainloop(Engine& engine) : engine(engine) { +} + +TestMainloop::~TestMainloop() = default; + +void TestMainloop::run() { + const auto& coreParams = engine.getCoreParameters(); + auto& time = engine.getTime(); + + if (coreParams.testFile.empty()) { + logger.info() << "nothing to do"; + return; + } + engine.setLevelConsumer([this](auto level) { + setLevel(std::move(level)); + }); + + logger.info() << "starting test " << coreParams.testFile; + auto process = scripting::start_coroutine(coreParams.testFile); + while (process->isActive()) { + time.step(1.0f / static_cast(TPS)); + process->update(); + if (controller) { + float delta = time.getDelta(); + controller->getLevel()->getWorld()->updateTimers(delta); + controller->update(glm::min(delta, 0.2f), false); + } + } + logger.info() << "test finished"; +} + +void TestMainloop::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)); + } +} diff --git a/src/TestMainloop.hpp b/src/TestMainloop.hpp new file mode 100644 index 000000000..06ffae25a --- /dev/null +++ b/src/TestMainloop.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include + +class Level; +class LevelController; +class Engine; + +class TestMainloop { + Engine& engine; + std::unique_ptr controller; +public: + TestMainloop(Engine& engine); + ~TestMainloop(); + + void run(); + + void setLevel(std::unique_ptr level); +}; diff --git a/src/Time.hpp b/src/Time.hpp new file mode 100644 index 000000000..9f914b1fc --- /dev/null +++ b/src/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/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/content/ContentBuilder.cpp b/src/content/ContentBuilder.cpp index f7ab3b1a7..633085788 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/ContentPack.cpp b/src/content/ContentPack.cpp index 166400c6b..72d4540f4 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(), {} }; } @@ -146,9 +146,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..a3b402118 100644 --- a/src/content/ContentPack.hpp +++ b/src/content/ContentPack.hpp @@ -71,7 +71,7 @@ struct ContentPack { const std::string& name ); - static ContentPack createCore(const EnginePaths*); + static ContentPack createCore(const EnginePaths&); static inline fs::path getFolderFor(ContentType type) { switch (type) { 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..38160dac8 100644 --- a/src/core_defs.hpp +++ b/src/core_defs.hpp @@ -35,5 +35,5 @@ class EnginePaths; class ContentBuilder; namespace corecontent { - void setup(EnginePaths* paths, ContentBuilder* builder); + void setup(const EnginePaths& paths, ContentBuilder& builder); } diff --git a/src/engine.cpp b/src/engine.cpp index 129dc68db..3c7d4bb5e 100644 --- a/src/engine.cpp +++ b/src/engine.cpp @@ -15,13 +15,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" @@ -37,7 +34,9 @@ #include "window/Events.hpp" #include "window/input.hpp" #include "window/Window.hpp" -#include "settings.hpp" +#include "world/Level.hpp" +#include "Mainloop.hpp" +#include "TestMainloop.hpp" #include #include @@ -71,42 +70,54 @@ 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(); 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()); + 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); - } keepAlive(settings.ui.language.observe([=](auto lang) { setLanguage(lang); }, true)); @@ -116,7 +127,7 @@ Engine::Engine(EngineSettings& settings, SettingsHandler& settingsHandler, Engin } 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 +141,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 +154,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,50 +166,47 @@ 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)); +void Engine::run() { + if (params.headless) { + TestMainloop(*this).run(); + } else { + Mainloop(*this).run(); + } +} - Batch2D batch(1024); - lastTime = Window::time(); - - logger.info() << "engine started"; - while (!Window::isShouldClose()){ - assert(screen != nullptr); - updateTimers(); - updateHotkeys(); - audio::update(delta); - - gui->act(delta, Viewport(Window::width, Window::height)); - screen->update(delta); - - if (!Window::isIconified()) { - renderFrame(batch); - } - Window::setFramerate( - Window::isIconified() && settings.display.limitFpsIconified.get() - ? 20 - : settings.display.framerate.get() - ); +void Engine::postUpdate() { + network->update(); + processPostRunnables(); +} - 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); } @@ -220,9 +221,11 @@ void Engine::processPostRunnables() { 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"; } @@ -257,12 +266,16 @@ 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") + paths.getUserFilesFolder()/fs::path("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()); @@ -288,32 +301,31 @@ void Engine::loadAssets() { } } 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 +341,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,10 +349,10 @@ 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); contentPacks = manager.getAll(names); @@ -372,13 +384,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); @@ -407,26 +421,22 @@ void Engine::loadWorldContent(const fs::path& folder) { PacksManager manager; manager.setSources({ folder/fs::path("content"), - paths->getUserFilesFolder()/fs::path("content"), - paths->getResourcesFolder()/fs::path("content") + paths.getUserFilesFolder()/fs::path("content"), + paths.getResourcesFolder()/fs::path("content") }); manager.scan(); contentPacks = manager.getAll(manager.assembly(packNames)); - paths->setCurrentWorldFolder(folder); + 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; -} - void Engine::setScreen(std::shared_ptr screen) { // reset audio channels (stop all sources) audio::reset_channel(audio::get_channel_index("regular")); @@ -435,8 +445,20 @@ 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); + if (gui) { + gui->getMenu()->setPageLoader(menus::create_page_loader(this)); + } +} + +void Engine::onWorldOpen(std::unique_ptr level) { + logger.info() << "world open"; + levelConsumer(std::move(level)); +} + +void Engine::onWorldClosed() { + logger.info() << "world closed"; + levelConsumer(nullptr); } gui::GUI* Engine::getGUI() { @@ -470,7 +492,7 @@ std::vector& Engine::getBasePacks() { } EnginePaths* Engine::getPaths() { - return paths; + return &paths; } ResPaths* Engine::getResPaths() { @@ -493,3 +515,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.hpp index 04a221dff..63a780845 100644 --- a/src/engine.hpp +++ b/src/engine.hpp @@ -2,13 +2,16 @@ #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 "Time.hpp" #include #include @@ -18,16 +21,14 @@ #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,10 +46,18 @@ class initialize_error : public std::runtime_error { initialize_error(const std::string& message) : std::runtime_error(message) {} }; +struct CoreParameters { + bool headless = false; + std::filesystem::path resFolder {"res"}; + std::filesystem::path userFolder {"."}; + std::filesystem::path testFile; +}; + 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; @@ -61,28 +70,28 @@ class Engine : public util::ObjectsKeeper { 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; + Time time; + consumer> levelConsumer; 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(); @@ -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(); @@ -128,6 +134,9 @@ class Engine : public util::ObjectsKeeper { /// @brief Get engine resource paths controller ResPaths* getResPaths(); + void onWorldOpen(std::unique_ptr level); + void onWorldClosed(); + /// @brief Get current Content instance const Content* getContent() const; @@ -151,7 +160,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/files/engine_paths.cpp b/src/files/engine_paths.cpp index d92518970..268573313 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); diff --git a/src/frontend/debug_panel.cpp b/src/frontend/debug_panel.cpp index 9043f5b9d..3470cdb14 100644 --- a/src/frontend/debug_panel.cpp +++ b/src/frontend/debug_panel.cpp @@ -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" @@ -56,7 +57,7 @@ std::shared_ptr create_debug_panel( static std::wstring fpsString = L""; panel->listenInterval(0.016f, [engine]() { - fps = 1.0f / engine->getDelta(); + fps = 1.0f / engine->getTime().getDelta(); fpsMin = std::min(fps, fpsMin); fpsMax = std::max(fps, fpsMax); }); @@ -95,7 +96,7 @@ std::shared_ptr create_debug_panel( std::to_wstring(ParticlesRenderer::aliveEmitters); })); panel->add(create_label([&]() { - return L"chunks: "+std::to_wstring(level.chunks->getChunksCount())+ + return L"chunks: "+std::to_wstring(level.chunksStorage->size())+ L" visible: "+std::to_wstring(ChunksRenderer::visibleChunks); })); panel->add(create_label([&]() { diff --git a/src/frontend/hud.cpp b/src/frontend/hud.cpp index 21111e187..1a2d83150 100644 --- a/src/frontend/hud.cpp +++ b/src/frontend/hud.cpp @@ -40,7 +40,7 @@ #include "voxels/Block.hpp" #include "voxels/Chunk.hpp" #include "voxels/Chunks.hpp" -#include "voxels/ChunksStorage.hpp" +#include "voxels/GlobalChunks.hpp" #include "window/Camera.hpp" #include "window/Events.hpp" #include "window/input.hpp" @@ -269,6 +269,7 @@ void Hud::updateHotbarControl() { void Hud::updateWorldGenDebugVisualization() { auto& level = frontend.getLevel(); + auto& chunks = *level.chunks; auto generator = frontend.getController()->getChunksController()->getGenerator(); auto debugInfo = generator->createDebugInfo(); @@ -293,9 +294,9 @@ void Hud::updateWorldGenDebugVisualization() { int az = z - (height - areaHeight) / 2; data[(flippedZ * width + x) * 4 + 1] = - level.chunks->getChunk(ax + ox, az + oz) ? 255 : 0; + chunks.getChunk(ax + ox, az + oz) ? 255 : 0; data[(flippedZ * width + x) * 4 + 0] = - level.chunksStorage->get(ax + ox, az + oz) ? 255 : 0; + level.chunksStorage->fetch(ax + ox, az + oz) ? 255 : 0; if (ax < 0 || az < 0 || ax >= areaWidth || az >= areaHeight) { diff --git a/src/frontend/locale.cpp b/src/frontend/locale.cpp index de040a11d..2af780769 100644 --- a/src/frontend/locale.cpp +++ b/src/frontend/locale.cpp @@ -37,38 +37,40 @@ const std::string& langs::Lang::getId() const { return locale; } -// @brief Language key-value txt files parser -class Reader : BasicParser { - void skipWhitespace() override { - BasicParser::skipWhitespace(); - if (hasNext() && source[pos] == '#') { - skipLine(); - if (hasNext() && is_whitespace(peek())) { - skipWhitespace(); +/// @brief Language key-value txt files parser +namespace { + class Reader : BasicParser { + void skipWhitespace() override { + BasicParser::skipWhitespace(); + if (hasNext() && source[pos] == '#') { + skipLine(); + if (hasNext() && is_whitespace(peek())) { + skipWhitespace(); + } } } - } -public: - Reader(std::string_view file, std::string_view source) : BasicParser(file, source) { - } + public: + Reader(std::string_view file, std::string_view source) + : BasicParser(file, source) { + } - void read(langs::Lang& lang, const std::string &prefix) { - skipWhitespace(); - while (hasNext()) { - std::string key = parseString('=', true); - util::trim(key); - key = prefix + key; - std::string text = parseString('\n', false); - util::trim(text); - lang.put(util::str2wstr_utf8(key), - util::str2wstr_utf8(text)); + void read(langs::Lang& lang, const std::string &prefix) { skipWhitespace(); + while (hasNext()) { + std::string key = parseString('=', true); + util::trim(key); + key = prefix + key; + std::string text = parseString('\n', false); + util::trim(text); + lang.put(util::str2wstr_utf8(key), util::str2wstr_utf8(text)); + skipWhitespace(); + } } - } -}; + }; +} void langs::loadLocalesInfo(const fs::path& resdir, std::string& fallback) { - fs::path file = resdir/fs::path(langs::TEXTS_FOLDER)/fs::path("langs.json"); + auto file = resdir/fs::u8path(langs::TEXTS_FOLDER)/fs::u8path("langs.json"); auto root = files::read_json(file); langs::locales_info.clear(); @@ -85,7 +87,7 @@ void langs::loadLocalesInfo(const fs::path& resdir, std::string& fallback) { } else { continue; } - logline << "[" << key << " (" << name << ")] "; + logline << key << " "; langs::locales_info[key] = LocaleInfo {key, name}; } logline << "added"; diff --git a/src/frontend/screens/LevelScreen.cpp b/src/frontend/screens/LevelScreen.cpp index 34d1db678..9470e7e08 100644 --- a/src/frontend/screens/LevelScreen.cpp +++ b/src/frontend/screens/LevelScreen.cpp @@ -1,26 +1,30 @@ #include "LevelScreen.hpp" -#include "core_defs.hpp" -#include "frontend/hud.hpp" -#include "frontend/LevelFrontend.hpp" #include "audio/audio.hpp" #include "coders/imageio.hpp" +#include "content/Content.hpp" +#include "core_defs.hpp" #include "debug/Logger.hpp" #include "engine.hpp" #include "files/files.hpp" -#include "content/Content.hpp" +#include "frontend/LevelFrontend.hpp" +#include "frontend/hud.hpp" #include "graphics/core/DrawContext.hpp" #include "graphics/core/ImageData.hpp" #include "graphics/core/PostProcessing.hpp" #include "graphics/core/Viewport.hpp" -#include "graphics/render/WorldRenderer.hpp" #include "graphics/render/Decorator.hpp" -#include "graphics/ui/elements/Menu.hpp" +#include "graphics/render/WorldRenderer.hpp" #include "graphics/ui/GUI.hpp" +#include "graphics/ui/elements/Menu.hpp" #include "logic/LevelController.hpp" +#include "logic/PlayerController.hpp" +#include "logic/scripting/scripting.hpp" #include "logic/scripting/scripting_hud.hpp" -#include "util/stringutil.hpp" +#include "maths/voxmaths.hpp" +#include "objects/Players.hpp" #include "physics/Hitbox.hpp" +#include "util/stringutil.hpp" #include "voxels/Chunks.hpp" #include "window/Camera.hpp" #include "window/Events.hpp" @@ -41,16 +45,25 @@ LevelScreen::LevelScreen(Engine* engine, std::unique_ptr levelPtr) menu->reset(); controller = std::make_unique(engine, std::move(levelPtr)); + + auto player = level->players->get(0); + playerController = std::make_unique( + settings, + level, + player, + controller->getBlocksController() + ); + frontend = std::make_unique( - controller->getPlayer(), controller.get(), assets + player, controller.get(), assets ); worldRenderer = std::make_unique( - engine, *frontend, controller->getPlayer() + engine, *frontend, player ); - hud = std::make_unique(engine, *frontend, controller->getPlayer()); + hud = std::make_unique(engine, *frontend, player); decorator = std::make_unique( - *engine, *controller, *worldRenderer, assets + *engine, *controller, *worldRenderer, assets, *player ); keepAlive(settings.graphics.backlight.observe([=](bool) { @@ -58,7 +71,7 @@ LevelScreen::LevelScreen(Engine* engine, std::unique_ptr levelPtr) worldRenderer->clear(); })); keepAlive(settings.camera.fov.observe([=](double value) { - controller->getPlayer()->fpCamera->setFov(glm::radians(value)); + player->fpCamera->setFov(glm::radians(value)); })); keepAlive(Events::getBinding(BIND_CHUNKS_RELOAD).onactived.add([=](){ controller->getLevel()->chunks->saveAndClear(); @@ -105,7 +118,7 @@ void LevelScreen::saveWorldPreview() { try { logger.info() << "saving world preview"; auto paths = engine->getPaths(); - auto player = controller->getPlayer(); + auto player = playerController->getPlayer(); auto& settings = engine->getSettings(); int previewSize = settings.ui.worldPreviewSize.get(); @@ -128,6 +141,7 @@ void LevelScreen::saveWorldPreview() { } void LevelScreen::updateHotkeys() { + auto player = playerController->getPlayer(); auto& settings = engine->getSettings(); if (Events::jpressed(keycode::O)) { settings.graphics.frustumCulling.toggle(); @@ -136,7 +150,7 @@ void LevelScreen::updateHotkeys() { hudVisible = !hudVisible; } if (Events::jpressed(keycode::F3)) { - controller->getPlayer()->debug = !controller->getPlayer()->debug; + player->debug = !player->debug; } } @@ -150,7 +164,7 @@ void LevelScreen::update(float delta) { updateHotkeys(); } - auto player = controller->getPlayer(); + auto player = playerController->getPlayer(); auto camera = player->currentCamera; bool paused = hud->isPause(); @@ -166,22 +180,32 @@ void LevelScreen::update(float delta) { camera->dir, glm::vec3(0, 1, 0) ); + auto level = controller->getLevel(); + const auto& settings = engine->getSettings(); if (!hud->isPause()) { - controller->getLevel()->getWorld()->updateTimers(delta); + level->getWorld()->updateTimers(delta); animator->update(delta); } - controller->update(glm::min(delta, 0.2f), !inputLocked, hud->isPause()); + if (!hud->isPause()) { + playerController->update(delta, !inputLocked); + } + controller->update(glm::min(delta, 0.2f), hud->isPause()); + playerController->postUpdate(delta, !inputLocked, hud->isPause()); + hud->update(hudVisible); decorator->update(delta, *camera); } void LevelScreen::draw(float delta) { - auto camera = controller->getPlayer()->currentCamera; + auto camera = playerController->getPlayer()->currentCamera; Viewport viewport(Window::width, Window::height); DrawContext ctx(nullptr, viewport, batch.get()); + if (!hud->isPause()) { + scripting::on_entities_render(engine->getTime().getDelta()); + } worldRenderer->draw( ctx, *camera, hudVisible, hud->isPause(), delta, postProcessing.get() ); diff --git a/src/frontend/screens/LevelScreen.hpp b/src/frontend/screens/LevelScreen.hpp index db0f04ff4..1810359b0 100644 --- a/src/frontend/screens/LevelScreen.hpp +++ b/src/frontend/screens/LevelScreen.hpp @@ -8,6 +8,7 @@ class Engine; class LevelFrontend; class Hud; class LevelController; +class PlayerController; class WorldRenderer; class TextureAnimator; class PostProcessing; @@ -18,6 +19,7 @@ class Level; class LevelScreen : public Screen { std::unique_ptr frontend; std::unique_ptr controller; + std::unique_ptr playerController; std::unique_ptr worldRenderer; std::unique_ptr animator; std::unique_ptr postProcessing; diff --git a/src/graphics/core/DrawContext.cpp b/src/graphics/core/DrawContext.cpp index 7062ad56c..936de95b0 100644 --- a/src/graphics/core/DrawContext.cpp +++ b/src/graphics/core/DrawContext.cpp @@ -92,6 +92,9 @@ DrawContext DrawContext::sub(Flushable* flushable) const { ctx.parent = this; ctx.flushable = flushable; ctx.scissorsCount = 0; + if (auto batch2D = dynamic_cast(flushable)) { + ctx.g2d = batch2D; + } return ctx; } diff --git a/src/graphics/core/DrawContext.hpp b/src/graphics/core/DrawContext.hpp index 736b053ee..6c20f37f5 100644 --- a/src/graphics/core/DrawContext.hpp +++ b/src/graphics/core/DrawContext.hpp @@ -10,7 +10,7 @@ class Framebuffer; class DrawContext { const DrawContext* parent; Viewport viewport; - Batch2D* const g2d; + Batch2D* g2d; Flushable* flushable = nullptr; Framebuffer* fbo = nullptr; bool depthMask = true; diff --git a/src/graphics/render/Decorator.cpp b/src/graphics/render/Decorator.cpp index b7bd42188..2f01955fb 100644 --- a/src/graphics/render/Decorator.cpp +++ b/src/graphics/render/Decorator.cpp @@ -10,6 +10,7 @@ #include "voxels/Block.hpp" #include "world/Level.hpp" #include "window/Camera.hpp" +#include "objects/Player.hpp" #include "objects/Players.hpp" #include "logic/LevelController.hpp" #include "util/stringutil.hpp" @@ -29,13 +30,17 @@ inline constexpr int ITERATIONS = 512; inline constexpr int BIG_PRIME = 666667; Decorator::Decorator( - Engine& engine, LevelController& controller, WorldRenderer& renderer, const Assets& assets + Engine& engine, + LevelController& controller, + WorldRenderer& renderer, + const Assets& assets, + Player& player ) : engine(engine), level(*controller.getLevel()), renderer(renderer), assets(assets), - player(*controller.getPlayer()) { + player(player) { controller.getBlocksController()->listenBlockInteraction( [this](auto player, const auto& pos, const auto& def, BlockInteraction type) { if (type == BlockInteraction::placing && def.particles) { @@ -43,7 +48,7 @@ Decorator::Decorator( } }); for (const auto& [id, player] : *level.players) { - if (id == controller.getPlayer()->getId()) { + if (id == this->player.getId()) { continue; } playerTexts[id] = renderer.texts->add(std::make_unique( diff --git a/src/graphics/render/Decorator.hpp b/src/graphics/render/Decorator.hpp index fbc196aa9..d19ed45f8 100644 --- a/src/graphics/render/Decorator.hpp +++ b/src/graphics/render/Decorator.hpp @@ -39,7 +39,8 @@ class Decorator { Engine& engine, LevelController& level, WorldRenderer& renderer, - const Assets& assets + const Assets& assets, + Player& player ); void update(float delta, const Camera& camera); diff --git a/src/graphics/render/WorldRenderer.cpp b/src/graphics/render/WorldRenderer.cpp index 19d6d232f..a0fd4c83d 100644 --- a/src/graphics/render/WorldRenderer.cpp +++ b/src/graphics/render/WorldRenderer.cpp @@ -86,7 +86,7 @@ WorldRenderer::WorldRenderer( auto& settings = engine->getSettings(); level.events->listen( EVT_CHUNK_HIDDEN, - [this](lvl_event_type, Chunk* chunk) { chunks->unload(chunk); } + [this](LevelEventType, Chunk* chunk) { chunks->unload(chunk); } ); auto assets = engine->getAssets(); skybox = std::make_unique( diff --git a/src/graphics/ui/GUI.cpp b/src/graphics/ui/GUI.cpp index 38bc22573..49a362e3e 100644 --- a/src/graphics/ui/GUI.cpp +++ b/src/graphics/ui/GUI.cpp @@ -23,7 +23,7 @@ using namespace gui; -GUI::GUI() { +GUI::GUI() : batch2D(std::make_unique(1024)) { container = std::make_shared(glm::vec2(1000)); uicamera = std::make_unique(glm::vec3(), Window::height); uicamera->perspective = false; @@ -198,7 +198,9 @@ void GUI::act(float delta, const Viewport& vp) { } void GUI::draw(const DrawContext& pctx, const Assets& assets) { - auto& viewport = pctx.getViewport(); + auto ctx = pctx.sub(batch2D.get()); + + auto& viewport = ctx.getViewport(); glm::vec2 wsize = viewport.size(); menu->setPos((wsize - menu->getSize()) / 2.0f); @@ -208,8 +210,8 @@ void GUI::draw(const DrawContext& pctx, const Assets& assets) { uishader->use(); uishader->uniformMatrix("u_projview", uicamera->getProjView()); - pctx.getBatch2D()->begin(); - container->draw(pctx, assets); + batch2D->begin(); + container->draw(ctx, assets); } std::shared_ptr GUI::getFocused() const { diff --git a/src/graphics/ui/GUI.hpp b/src/graphics/ui/GUI.hpp index c0dec6490..851a49f7d 100644 --- a/src/graphics/ui/GUI.hpp +++ b/src/graphics/ui/GUI.hpp @@ -13,6 +13,7 @@ class Viewport; class DrawContext; class Assets; class Camera; +class Batch2D; /* Some info about padding and margin. @@ -52,6 +53,7 @@ namespace gui { /// @brief The main UI controller class GUI { + std::unique_ptr batch2D; std::shared_ptr container; std::shared_ptr hover; std::shared_ptr pressed; diff --git a/src/interfaces/Process.hpp b/src/interfaces/Process.hpp new file mode 100644 index 000000000..b62b1e0f0 --- /dev/null +++ b/src/interfaces/Process.hpp @@ -0,0 +1,12 @@ +#pragma once + +/// @brief Process interface. +class Process { +public: + virtual ~Process() {} + + virtual bool isActive() const = 0; + virtual void update() = 0; + virtual void waitForEnd() = 0; + virtual void terminate() = 0; +}; diff --git a/src/lighting/Lighting.cpp b/src/lighting/Lighting.cpp index 6aad3049d..8d496b476 100644 --- a/src/lighting/Lighting.cpp +++ b/src/lighting/Lighting.cpp @@ -86,11 +86,12 @@ void Lighting::buildSkyLight(int cx, int cz){ solverS->solve(); } -void Lighting::onChunkLoaded(int cx, int cz, bool expand){ - LightSolver* solverR = this->solverR.get(); - LightSolver* solverG = this->solverG.get(); - LightSolver* solverB = this->solverB.get(); - LightSolver* solverS = this->solverS.get(); + +void Lighting::onChunkLoaded(int cx, int cz, bool expand) { + auto& solverR = *this->solverR; + auto& solverG = *this->solverG; + auto& solverB = *this->solverB; + auto& solverS = *this->solverS; auto blockDefs = content->getIndices()->blocks.getDefs(); auto chunk = chunks->getChunk(cx, cz); @@ -103,9 +104,9 @@ void Lighting::onChunkLoaded(int cx, int cz, bool expand){ int gx = x + cx * CHUNK_W; int gz = z + cz * CHUNK_D; if (block->rt.emissive){ - solverR->add(gx,y,gz,block->emission[0]); - solverG->add(gx,y,gz,block->emission[1]); - solverB->add(gx,y,gz,block->emission[2]); + solverR.add(gx,y,gz,block->emission[0]); + solverG.add(gx,y,gz,block->emission[1]); + solverB.add(gx,y,gz,block->emission[2]); } } } @@ -119,10 +120,10 @@ void Lighting::onChunkLoaded(int cx, int cz, bool expand){ int gz = z + cz * CHUNK_D; int rgbs = chunk->lightmap.get(x, y, z); if (rgbs){ - solverR->add(gx,y,gz, Lightmap::extract(rgbs, 0)); - solverG->add(gx,y,gz, Lightmap::extract(rgbs, 1)); - solverB->add(gx,y,gz, Lightmap::extract(rgbs, 2)); - solverS->add(gx,y,gz, Lightmap::extract(rgbs, 3)); + solverR.add(gx,y,gz, Lightmap::extract(rgbs, 0)); + solverG.add(gx,y,gz, Lightmap::extract(rgbs, 1)); + solverB.add(gx,y,gz, Lightmap::extract(rgbs, 2)); + solverS.add(gx,y,gz, Lightmap::extract(rgbs, 3)); } } } @@ -134,19 +135,19 @@ void Lighting::onChunkLoaded(int cx, int cz, bool expand){ int gz = z + cz * CHUNK_D; int rgbs = chunk->lightmap.get(x, y, z); if (rgbs){ - solverR->add(gx,y,gz, Lightmap::extract(rgbs, 0)); - solverG->add(gx,y,gz, Lightmap::extract(rgbs, 1)); - solverB->add(gx,y,gz, Lightmap::extract(rgbs, 2)); - solverS->add(gx,y,gz, Lightmap::extract(rgbs, 3)); + solverR.add(gx,y,gz, Lightmap::extract(rgbs, 0)); + solverG.add(gx,y,gz, Lightmap::extract(rgbs, 1)); + solverB.add(gx,y,gz, Lightmap::extract(rgbs, 2)); + solverS.add(gx,y,gz, Lightmap::extract(rgbs, 3)); } } } } } - solverR->solve(); - solverG->solve(); - solverB->solve(); - solverS->solve(); + solverR.solve(); + solverG.solve(); + solverB.solve(); + solverS.solve(); } void Lighting::onBlockSet(int x, int y, int z, blockid_t id){ diff --git a/src/lighting/Lightmap.cpp b/src/lighting/Lightmap.cpp index cb077b8b1..24581e246 100644 --- a/src/lighting/Lightmap.cpp +++ b/src/lighting/Lightmap.cpp @@ -2,16 +2,15 @@ #include "util/data_io.hpp" -#include +#include +#include void Lightmap::set(const Lightmap* lightmap) { set(lightmap->map); } void Lightmap::set(const light_t* map) { - for (size_t i = 0; i < CHUNK_VOL; i++) { - this->map[i] = map[i]; - } + std::memcpy(this->map, map, sizeof(light_t) * CHUNK_VOL); } static_assert(sizeof(light_t) == 2, "replace dataio calls to new light_t"); diff --git a/src/logic/ChunksController.cpp b/src/logic/ChunksController.cpp index f61a27550..0d8d97b4a 100644 --- a/src/logic/ChunksController.cpp +++ b/src/logic/ChunksController.cpp @@ -14,7 +14,7 @@ #include "voxels/Block.hpp" #include "voxels/Chunk.hpp" #include "voxels/Chunks.hpp" -#include "voxels/ChunksStorage.hpp" +#include "voxels/GlobalChunks.hpp" #include "world/Level.hpp" #include "world/World.hpp" #include "world/generator/WorldGenerator.hpp" diff --git a/src/logic/EngineController.cpp b/src/logic/EngineController.cpp index 4c38fc786..8c649e8ac 100644 --- a/src/logic/EngineController.cpp +++ b/src/logic/EngineController.cpp @@ -16,6 +16,7 @@ #include "frontend/screens/MenuScreen.hpp" #include "graphics/ui/elements/Menu.hpp" #include "graphics/ui/gui_util.hpp" +#include "objects/Players.hpp" #include "interfaces/Task.hpp" #include "util/stringutil.hpp" #include "world/Level.hpp" @@ -132,22 +133,25 @@ static void show_content_missing( menus::show(engine, "reports/missing_content", {std::move(root)}); } -static bool loadWorldContent(Engine* engine, const fs::path& folder) { - return menus::call(engine, [engine, folder]() { +static bool load_world_content(Engine* engine, const fs::path& folder) { + if (engine->isHeadless()) { engine->loadWorldContent(folder); - }); + return true; + } else { + return menus::call(engine, [engine, folder]() { + engine->loadWorldContent(folder); + }); + } } -static void loadWorld(Engine* engine, const std::shared_ptr& worldFiles) { +static void load_world(Engine* engine, const std::shared_ptr& worldFiles) { try { auto content = engine->getContent(); auto& packs = engine->getContentPacks(); auto& settings = engine->getSettings(); auto level = World::load(worldFiles, settings, content, packs); - engine->setScreen( - std::make_shared(engine, std::move(level)) - ); + engine->onWorldOpen(std::move(level)); } catch (const world_load_error& error) { guiutil::alert( engine->getGUI(), @@ -160,7 +164,12 @@ static void loadWorld(Engine* engine, const std::shared_ptr& worldFi void EngineController::openWorld(const std::string& name, bool confirmConvert) { auto paths = engine->getPaths(); auto folder = paths->getWorldsFolder() / fs::u8path(name); - if (!loadWorldContent(engine, folder)) { + auto worldFile = folder / fs::u8path("world.json"); + if (!fs::exists(worldFile)) { + throw std::runtime_error(worldFile.u8string() + " does not exists"); + } + + if (!load_world_content(engine, folder)) { return; } @@ -192,7 +201,7 @@ void EngineController::openWorld(const std::string& name, bool confirmConvert) { } return; } - loadWorld(engine, std::move(worldFiles)); + load_world(engine, std::move(worldFiles)); } inline uint64_t str2seed(const std::string& seedstr) { @@ -219,7 +228,10 @@ void EngineController::createWorld( EnginePaths* paths = engine->getPaths(); auto folder = paths->getWorldsFolder() / fs::u8path(name); - if (!menus::call(engine, [this, paths, folder]() { + if (engine->isHeadless()) { + engine->loadContent(); + paths->setCurrentWorldFolder(folder); + } else if (!menus::call(engine, [this, paths, folder]() { engine->loadContent(); paths->setCurrentWorldFolder(folder); })) { @@ -234,7 +246,10 @@ void EngineController::createWorld( engine->getContent(), engine->getContentPacks() ); - engine->setScreen(std::make_shared(engine, std::move(level))); + if (!engine->isHeadless()) { + level->players->create(); + } + engine->onWorldOpen(std::move(level)); } void EngineController::reopenWorld(World* world) { diff --git a/src/logic/LevelController.cpp b/src/logic/LevelController.cpp index 5d2ddd9b5..a9c002b19 100644 --- a/src/logic/LevelController.cpp +++ b/src/logic/LevelController.cpp @@ -5,13 +5,15 @@ #include "debug/Logger.hpp" #include "engine.hpp" #include "files/WorldFiles.hpp" +#include "maths/voxmaths.hpp" #include "objects/Entities.hpp" +#include "objects/Players.hpp" +#include "objects/Player.hpp" #include "physics/Hitbox.hpp" +#include "scripting/scripting.hpp" #include "settings.hpp" #include "world/Level.hpp" #include "world/World.hpp" -#include "maths/voxmaths.hpp" -#include "scripting/scripting.hpp" static debug::Logger logger("level-control"); @@ -23,39 +25,36 @@ LevelController::LevelController(Engine* engine, std::unique_ptr levelPtr )), chunks(std::make_unique( *level, settings.chunks.padding.get() - )), - player(std::make_unique( - settings, level.get(), blocks.get() )) { scripting::on_world_load(this); } -void LevelController::update(float delta, bool input, bool pause) { - glm::vec3 position = player->getPlayer()->getPosition(); - level->loadMatrix( - position.x, - position.z, - settings.chunks.loadDistance.get() + settings.chunks.padding.get() * 2 - ); - chunks->update( - settings.chunks.loadSpeed.get(), settings.chunks.loadDistance.get(), - floordiv(position.x, CHUNK_W), floordiv(position.z, CHUNK_D) - ); - +void LevelController::update(float delta, bool pause) { + for (const auto& [uid, player] : *level->players) { + glm::vec3 position = player->getPosition(); + level->loadMatrix( + position.x, + position.z, + settings.chunks.loadDistance.get() + settings.chunks.padding.get() * 2 + ); + chunks->update( + settings.chunks.loadSpeed.get(), settings.chunks.loadDistance.get(), + floordiv(position.x, CHUNK_W), floordiv(position.z, CHUNK_D) + ); + } if (!pause) { // update all objects that needed blocks->update(delta); - player->update(delta, input, pause); level->entities->updatePhysics(delta); level->entities->update(delta); } level->entities->clean(); - player->postUpdate(delta, input, pause); } void LevelController::saveWorld() { - level->getWorld()->wfile->createDirectories(); - logger.info() << "writing world"; + auto world = level->getWorld(); + logger.info() << "writing world '" << world->getName() << "'"; + world->wfile->createDirectories(); scripting::on_world_save(); level->onSave(); level->getWorld()->write(level.get()); @@ -69,10 +68,6 @@ Level* LevelController::getLevel() { return level.get(); } -Player* LevelController::getPlayer() { - return player->getPlayer(); -} - BlocksController* LevelController::getBlocksController() { return blocks.get(); } @@ -80,7 +75,3 @@ BlocksController* LevelController::getBlocksController() { ChunksController* LevelController::getChunksController() { return chunks.get(); } - -PlayerController* LevelController::getPlayerController() { - return player.get(); -} diff --git a/src/logic/LevelController.hpp b/src/logic/LevelController.hpp index 94e0ac2c5..6cfcf8167 100644 --- a/src/logic/LevelController.hpp +++ b/src/logic/LevelController.hpp @@ -4,7 +4,6 @@ #include "BlocksController.hpp" #include "ChunksController.hpp" -#include "PlayerController.hpp" class Engine; class Level; @@ -18,23 +17,19 @@ class LevelController { // Sub-controllers std::unique_ptr blocks; std::unique_ptr chunks; - std::unique_ptr player; public: LevelController(Engine* engine, std::unique_ptr level); /// @param delta time elapsed since the last update - /// @param input is user input allowed to be handled /// @param pause is world and player simulation paused - void update(float delta, bool input, bool pause); + void update(float delta, bool pause); void saveWorld(); void onWorldQuit(); Level* getLevel(); - Player* getPlayer(); BlocksController* getBlocksController(); ChunksController* getChunksController(); - PlayerController* getPlayerController(); }; diff --git a/src/logic/PlayerController.cpp b/src/logic/PlayerController.cpp index 96fed03ab..827b42c1e 100644 --- a/src/logic/PlayerController.cpp +++ b/src/logic/PlayerController.cpp @@ -170,18 +170,21 @@ void CameraControl::update(PlayerInput input, float delta, Chunks* chunks) { refresh(); + camera->updateVectors(); if (player->currentCamera == spCamera) { spCamera->position = chunks->rayCastToObstacle(camera->position, camera->front, 3.0f) - 0.4f * camera->front; spCamera->dir = -camera->dir; spCamera->front = -camera->front; + spCamera->right = -camera->right; } else if (player->currentCamera == tpCamera) { tpCamera->position = chunks->rayCastToObstacle(camera->position, -camera->front, 3.0f) + 0.4f * camera->front; tpCamera->dir = camera->dir; tpCamera->front = camera->front; + tpCamera->right = camera->right; } if (player->currentCamera == spCamera || player->currentCamera == tpCamera || player->currentCamera == camera) { @@ -190,14 +193,17 @@ void CameraControl::update(PlayerInput input, float delta, Chunks* chunks) { } PlayerController::PlayerController( - const EngineSettings& settings, Level* level, + const EngineSettings& settings, + Level* level, + Player* player, BlocksController* blocksController ) - : settings(settings), level(level), - player(level->players->get(0)), + : settings(settings), + level(level), + player(player), camControl(player, settings.camera), blocksController(blocksController), - playerTickClock(60, 1) { + playerTickClock(20, 3) { } void PlayerController::onFootstep(const Hitbox& hitbox) { @@ -242,21 +248,19 @@ void PlayerController::updateFootsteps(float delta) { } } -void PlayerController::update(float delta, bool input, bool pause) { - if (!pause) { - if (input) { - updateKeyboard(); - player->updateSelectedEntity(); - } else { - resetKeyboard(); - } - updatePlayer(delta); +void PlayerController::update(float delta, bool input) { + if (input) { + updateKeyboard(); + player->updateSelectedEntity(); + } else { + resetKeyboard(); + } + updatePlayer(delta); - if (playerTickClock.update(delta)) { - if (player->getId() % playerTickClock.getParts() == - playerTickClock.getPart()) { - scripting::on_player_tick(player, playerTickClock.getTickRate()); - } + if (playerTickClock.update(delta)) { + if (player->getId() % playerTickClock.getParts() == + playerTickClock.getPart()) { + scripting::on_player_tick(player, playerTickClock.getTickRate()); } } } diff --git a/src/logic/PlayerController.hpp b/src/logic/PlayerController.hpp index 8d82a3f99..9ae8d7271 100644 --- a/src/logic/PlayerController.hpp +++ b/src/logic/PlayerController.hpp @@ -71,9 +71,21 @@ class PlayerController { voxel* updateSelection(float maxDistance); public: PlayerController( - const EngineSettings& settings, Level* level, BlocksController* blocksController + const EngineSettings& settings, + Level* level, + Player* player, + BlocksController* blocksController ); - void update(float delta, bool input, bool pause); + + /// @brief Called after blocks update if not paused + /// @param delta delta time + /// @param input process user input + void update(float delta, bool input); + + /// @brief Called after whole level update + /// @param delta delta time + /// @param input process user input + /// @param pause is game paused void postUpdate(float delta, bool input, bool pause); Player* getPlayer(); }; diff --git a/src/logic/scripting/lua/libs/api_lua.hpp b/src/logic/scripting/lua/libs/api_lua.hpp index a3b2d152c..23f3db66a 100644 --- a/src/logic/scripting/lua/libs/api_lua.hpp +++ b/src/logic/scripting/lua/libs/api_lua.hpp @@ -37,6 +37,7 @@ extern const luaL_Reg packlib[]; extern const luaL_Reg particleslib[]; // gfx.particles extern const luaL_Reg playerlib[]; extern const luaL_Reg quatlib[]; +extern const luaL_Reg testlib[]; extern const luaL_Reg text3dlib[]; // gfx.text3d extern const luaL_Reg timelib[]; extern const luaL_Reg tomllib[]; diff --git a/src/logic/scripting/lua/libs/libblock.cpp b/src/logic/scripting/lua/libs/libblock.cpp index 2f24e0feb..dbd87489a 100644 --- a/src/logic/scripting/lua/libs/libblock.cpp +++ b/src/logic/scripting/lua/libs/libblock.cpp @@ -7,6 +7,7 @@ #include "voxels/Chunk.hpp" #include "voxels/Chunks.hpp" #include "voxels/voxel.hpp" +#include "voxels/GlobalChunks.hpp" #include "world/Level.hpp" #include "maths/voxmaths.hpp" #include "data/StructLayout.hpp" @@ -114,11 +115,20 @@ static int l_get(lua::State* L) { return lua::pushinteger(L, id); } +static int l_get_slow(lua::State* L) { + auto x = lua::tointeger(L, 1); + auto y = lua::tointeger(L, 2); + auto z = lua::tointeger(L, 3); + auto vox = level->chunksStorage->get(x, y, z); + int id = vox == nullptr ? -1 : vox->id; + return lua::pushinteger(L, id); +} + static int l_get_x(lua::State* L) { auto x = lua::tointeger(L, 1); auto y = lua::tointeger(L, 2); auto z = lua::tointeger(L, 3); - auto vox = level->chunks->get(x, y, z); + auto vox = level->chunksStorage->get(x, y, z); if (vox == nullptr) { return lua::pushivec_stack(L, glm::ivec3(1, 0, 0)); } @@ -135,7 +145,7 @@ static int l_get_y(lua::State* L) { auto x = lua::tointeger(L, 1); auto y = lua::tointeger(L, 2); auto z = lua::tointeger(L, 3); - auto vox = level->chunks->get(x, y, z); + auto vox = level->chunksStorage->get(x, y, z); if (vox == nullptr) { return lua::pushivec_stack(L, glm::ivec3(0, 1, 0)); } @@ -152,7 +162,7 @@ static int l_get_z(lua::State* L) { auto x = lua::tointeger(L, 1); auto y = lua::tointeger(L, 2); auto z = lua::tointeger(L, 3); - auto vox = level->chunks->get(x, y, z); + auto vox = level->chunksStorage->get(x, y, z); if (vox == nullptr) { return lua::pushivec_stack(L, glm::ivec3(0, 0, 1)); } @@ -169,7 +179,7 @@ static int l_get_rotation(lua::State* L) { auto x = lua::tointeger(L, 1); auto y = lua::tointeger(L, 2); auto z = lua::tointeger(L, 3); - voxel* vox = level->chunks->get(x, y, z); + voxel* vox = level->chunksStorage->get(x, y, z); int rotation = vox == nullptr ? 0 : vox->state.rotation; return lua::pushinteger(L, rotation); } @@ -187,7 +197,7 @@ static int l_get_states(lua::State* L) { auto x = lua::tointeger(L, 1); auto y = lua::tointeger(L, 2); auto z = lua::tointeger(L, 3); - auto vox = level->chunks->get(x, y, z); + auto vox = level->chunksStorage->get(x, y, z); int states = vox == nullptr ? 0 : blockstate2int(vox->state); return lua::pushinteger(L, states); } @@ -598,6 +608,7 @@ const luaL_Reg blocklib[] = { {"is_replaceable_at", lua::wrap}, {"set", lua::wrap}, {"get", lua::wrap}, + {"get_slow", lua::wrap}, {"get_X", lua::wrap}, {"get_Y", lua::wrap}, {"get_Z", lua::wrap}, diff --git a/src/logic/scripting/lua/libs/libcore.cpp b/src/logic/scripting/lua/libs/libcore.cpp index 9b048e443..5ac9d93d0 100644 --- a/src/logic/scripting/lua/libs/libcore.cpp +++ b/src/logic/scripting/lua/libs/libcore.cpp @@ -69,10 +69,7 @@ static int l_close_world(lua::State* L) { if (save_world) { controller->saveWorld(); } - // destroy LevelScreen and run quit callbacks - engine->setScreen(nullptr); - // create and go to menu screen - engine->setScreen(std::make_shared(engine)); + engine->onWorldClosed(); return 0; } @@ -118,8 +115,8 @@ static int l_reconfig_packs(lua::State* L) { remPacks.emplace_back(lua::tostring(L, -1)); lua::pop(L); } - auto engine_controller = engine->getController(); - engine_controller->reconfigPacks(controller, addPacks, remPacks); + auto engineController = engine->getController(); + engineController->reconfigPacks(controller, addPacks, remPacks); return 0; } @@ -237,7 +234,12 @@ static int l_quit(lua::State*) { return 0; } +static int l_blank(lua::State*) { + return 0; +} + const luaL_Reg corelib[] = { + {"nop", lua::wrap}, {"get_version", lua::wrap}, {"new_world", lua::wrap}, {"open_world", lua::wrap}, diff --git a/src/logic/scripting/lua/libs/libplayer.cpp b/src/logic/scripting/lua/libs/libplayer.cpp index 0628c5afe..00c4585d9 100644 --- a/src/logic/scripting/lua/libs/libplayer.cpp +++ b/src/logic/scripting/lua/libs/libplayer.cpp @@ -250,6 +250,14 @@ static int l_set_name(lua::State* L) { return 0; } +static int l_create(lua::State* L) { + auto player = level->players->create(); + if (lua::isstring(L, 1)) { + player->setName(lua::require_string(L, 1)); + } + return lua::pushinteger(L, player->getId()); +} + const luaL_Reg playerlib[] = { {"get_pos", lua::wrap}, {"set_pos", lua::wrap}, @@ -277,5 +285,6 @@ const luaL_Reg playerlib[] = { {"set_camera", lua::wrap}, {"get_name", lua::wrap}, {"set_name", lua::wrap}, + {"create", lua::wrap}, {NULL, NULL} }; diff --git a/src/logic/scripting/lua/libs/libtest.cpp b/src/logic/scripting/lua/libs/libtest.cpp new file mode 100644 index 000000000..034a72fb4 --- /dev/null +++ b/src/logic/scripting/lua/libs/libtest.cpp @@ -0,0 +1,5 @@ +#include "api_lua.hpp" + +const luaL_Reg testlib[] = { + {NULL, NULL} +}; diff --git a/src/logic/scripting/lua/libs/libtime.cpp b/src/logic/scripting/lua/libs/libtime.cpp index 93708dda5..78952ede8 100644 --- a/src/logic/scripting/lua/libs/libtime.cpp +++ b/src/logic/scripting/lua/libs/libtime.cpp @@ -1,16 +1,18 @@ #include "engine.hpp" -#include "window/Window.hpp" #include "api_lua.hpp" -static int l_time_uptime(lua::State* L) { - return lua::pushnumber(L, Window::time()); +using namespace scripting; + +static int l_uptime(lua::State* L) { + return lua::pushnumber(L, engine->getTime().getTime()); } -static int l_time_delta(lua::State* L) { - return lua::pushnumber(L, scripting::engine->getDelta()); +static int l_delta(lua::State* L) { + return lua::pushnumber(L, engine->getTime().getDelta()); } const luaL_Reg timelib[] = { - {"uptime", lua::wrap}, - {"delta", lua::wrap}, - {NULL, NULL}}; + {"uptime", lua::wrap}, + {"delta", lua::wrap}, + {NULL, NULL} +}; diff --git a/src/logic/scripting/lua/libs/libworld.cpp b/src/logic/scripting/lua/libs/libworld.cpp index 99d2fec3b..680047778 100644 --- a/src/logic/scripting/lua/libs/libworld.cpp +++ b/src/logic/scripting/lua/libs/libworld.cpp @@ -13,6 +13,7 @@ #include "lighting/Lighting.hpp" #include "voxels/Chunk.hpp" #include "voxels/Chunks.hpp" +#include "voxels/GlobalChunks.hpp" #include "world/Level.hpp" #include "world/World.hpp" @@ -231,6 +232,13 @@ static int l_set_chunk_data(lua::State* L) { return 1; } +static int l_count_chunks(lua::State* L) { + if (level == nullptr) { + return 0; + } + return lua::pushinteger(L, level->chunksStorage->size()); +} + const luaL_Reg worldlib[] = { {"is_open", lua::wrap}, {"get_list", lua::wrap}, @@ -246,5 +254,6 @@ const luaL_Reg worldlib[] = { {"exists", lua::wrap}, {"get_chunk_data", lua::wrap}, {"set_chunk_data", lua::wrap}, + {"count_chunks", lua::wrap}, {NULL, NULL} }; diff --git a/src/logic/scripting/lua/lua_commons.hpp b/src/logic/scripting/lua/lua_commons.hpp index ff94ade7c..f272dbf03 100644 --- a/src/logic/scripting/lua/lua_commons.hpp +++ b/src/logic/scripting/lua/lua_commons.hpp @@ -3,9 +3,8 @@ #include "delegates.hpp" #include "logic/scripting/scripting.hpp" -#ifdef __linux__ +#if (defined __linux__) || (defined __MINGW32__) #include - #include #else #include diff --git a/src/logic/scripting/lua/lua_engine.cpp b/src/logic/scripting/lua/lua_engine.cpp index 8f4777108..0a4e22597 100644 --- a/src/logic/scripting/lua/lua_engine.cpp +++ b/src/logic/scripting/lua/lua_engine.cpp @@ -9,6 +9,7 @@ #include "util/stringutil.hpp" #include "libs/api_lua.hpp" #include "lua_custom_types.hpp" +#include "engine.hpp" static debug::Logger logger("lua-state"); static lua::State* main_thread = nullptr; @@ -57,7 +58,10 @@ static void create_libs(State* L, StateType stateType) { openlib(L, "vec3", vec3lib); openlib(L, "vec4", vec4lib); - if (stateType == StateType::BASE) { + if (stateType == StateType::TEST) { + openlib(L, "test", testlib); + } + if (stateType == StateType::BASE || stateType == StateType::TEST) { openlib(L, "gui", guilib); openlib(L, "input", inputlib); openlib(L, "inventory", inventorylib); @@ -110,11 +114,15 @@ void lua::init_state(State* L, StateType stateType) { newusertype(L); } -void lua::initialize(const EnginePaths& paths) { +void lua::initialize(const EnginePaths& paths, const CoreParameters& params) { logger.info() << LUA_VERSION; logger.info() << LUAJIT_VERSION; - main_thread = create_state(paths, StateType::BASE); + main_thread = create_state( + paths, params.headless ? StateType::TEST : StateType::BASE + ); + lua::pushstring(main_thread, params.testFile.stem().u8string()); + lua::setglobal(main_thread, "__VC_TEST_NAME"); } void lua::finalize() { diff --git a/src/logic/scripting/lua/lua_engine.hpp b/src/logic/scripting/lua/lua_engine.hpp index dd1e794d4..a70f59481 100644 --- a/src/logic/scripting/lua/lua_engine.hpp +++ b/src/logic/scripting/lua/lua_engine.hpp @@ -8,14 +8,16 @@ #include "lua_util.hpp" class EnginePaths; +struct CoreParameters; namespace lua { enum class StateType { BASE, + TEST, GENERATOR, }; - void initialize(const EnginePaths& paths); + void initialize(const EnginePaths& paths, const CoreParameters& params); void finalize(); bool emit_event( diff --git a/src/logic/scripting/scripting.cpp b/src/logic/scripting/scripting.cpp index a8b5f070d..3d3bc9e42 100644 --- a/src/logic/scripting/scripting.cpp +++ b/src/logic/scripting/scripting.cpp @@ -25,6 +25,7 @@ #include "util/timeutil.hpp" #include "voxels/Block.hpp" #include "world/Level.hpp" +#include "interfaces/Process.hpp" using namespace scripting; @@ -65,12 +66,65 @@ int scripting::load_script( void scripting::initialize(Engine* engine) { scripting::engine = engine; - lua::initialize(*engine->getPaths()); + lua::initialize(*engine->getPaths(), engine->getCoreParameters()); load_script(fs::path("stdlib.lua"), true); load_script(fs::path("classes.lua"), true); } +class LuaCoroutine : public Process { + lua::State* L; + int id; + bool alive = true; +public: + LuaCoroutine(lua::State* L, int id) : L(L), id(id) { + } + + bool isActive() const override { + return alive; + } + + void update() override { + if (lua::getglobal(L, "__vc_resume_coroutine")) { + lua::pushinteger(L, id); + if (lua::call(L, 1)) { + alive = lua::toboolean(L, -1); + lua::pop(L); + } + } + } + + void waitForEnd() override { + while (isActive()) { + update(); + } + } + + void terminate() override { + if (lua::getglobal(L, "__vc_stop_coroutine")) { + lua::pushinteger(L, id); + lua::pop(L, lua::call(L, 1)); + } + } +}; + +std::unique_ptr scripting::start_coroutine( + const std::filesystem::path& script +) { + auto L = lua::get_main_state(); + if (lua::getglobal(L, "__vc_start_coroutine")) { + auto source = files::read_string(script); + lua::loadbuffer(L, 0, source, script.filename().u8string()); + if (lua::call(L, 1)) { + int id = lua::tointeger(L, -1); + lua::pop(L, 2); + return std::make_unique(L, id); + } + lua::pop(L); + } + return nullptr; +} + [[nodiscard]] scriptenv scripting::get_root_environment() { return std::make_shared(0); } @@ -368,7 +422,7 @@ bool scripting::on_block_interact( Player* player, const Block& block, const glm::ivec3& pos ) { std::string name = block.name + ".interact"; - return lua::emit_event(lua::get_main_state(), name, [pos, player](auto L) { + auto result = lua::emit_event(lua::get_main_state(), name, [pos, player](auto L) { lua::pushivec_stack(L, pos); lua::pushinteger(L, player->getId()); return 4; @@ -386,6 +440,7 @@ bool scripting::on_block_interact( ); } } + return result; } void scripting::on_player_tick(Player* player, int tps) { diff --git a/src/logic/scripting/scripting.hpp b/src/logic/scripting/scripting.hpp index 0ab3c0351..4ccba2cb0 100644 --- a/src/logic/scripting/scripting.hpp +++ b/src/logic/scripting/scripting.hpp @@ -11,8 +11,6 @@ #include "typedefs.hpp" #include "scripting_functional.hpp" -namespace fs = std::filesystem; - class Engine; class Content; struct ContentPack; @@ -34,6 +32,7 @@ class Entity; struct EntityDef; class GeneratorScript; struct GeneratorDef; +class Process; namespace scripting { extern Engine* engine; @@ -60,6 +59,10 @@ namespace scripting { void process_post_runnables(); + std::unique_ptr start_coroutine( + const std::filesystem::path& script + ); + void on_world_load(LevelController* controller); void on_world_tick(); void on_world_save(); @@ -136,7 +139,7 @@ namespace scripting { void load_content_script( const scriptenv& env, const std::string& prefix, - const fs::path& file, + const std::filesystem::path& file, const std::string& fileName, block_funcs_set& funcsset ); @@ -150,7 +153,7 @@ namespace scripting { void load_content_script( const scriptenv& env, const std::string& prefix, - const fs::path& file, + const std::filesystem::path& file, const std::string& fileName, item_funcs_set& funcsset ); @@ -161,13 +164,13 @@ namespace scripting { /// @param fileName script file path using the engine format void load_entity_component( const std::string& name, - const fs::path& file, + const std::filesystem::path& file, const std::string& fileName ); std::unique_ptr load_generator( const GeneratorDef& def, - const fs::path& file, + const std::filesystem::path& file, const std::string& dirPath ); @@ -179,7 +182,7 @@ namespace scripting { void load_world_script( const scriptenv& env, const std::string& packid, - const fs::path& file, + const std::filesystem::path& file, const std::string& fileName, world_funcs_set& funcsset ); @@ -193,7 +196,7 @@ namespace scripting { void load_layout_script( const scriptenv& env, const std::string& prefix, - const fs::path& file, + const std::filesystem::path& file, const std::string& fileName, uidocscript& script ); diff --git a/src/voxel_engine.cpp b/src/main.cpp similarity index 54% rename from src/voxel_engine.cpp rename to src/main.cpp index c6a2ec7ed..b0d034123 100644 --- a/src/voxel_engine.cpp +++ b/src/main.cpp @@ -1,32 +1,29 @@ #include "engine.hpp" -#include "settings.hpp" -#include "files/settings_io.hpp" -#include "files/engine_paths.hpp" #include "util/platform.hpp" #include "util/command_line.hpp" #include "debug/Logger.hpp" +#include #include static debug::Logger logger("main"); int main(int argc, char** argv) { - debug::Logger::init("latest.log"); - - EnginePaths paths; - if (!parse_cmdline(argc, argv, paths)) - return EXIT_SUCCESS; + CoreParameters coreParameters; + try { + if (!parse_cmdline(argc, argv, coreParameters)) { + return EXIT_SUCCESS; + } + } catch (const std::runtime_error& err) { + std::cerr << err.what() << std::endl; + return EXIT_FAILURE; + } + debug::Logger::init(coreParameters.userFolder.string()+"/latest.log"); platform::configure_encoding(); try { - EngineSettings settings; - SettingsHandler handler(settings); - - Engine engine(settings, handler, &paths); - - engine.mainloop(); - } - catch (const initialize_error& err) { + Engine(std::move(coreParameters)).run(); + } catch (const initialize_error& err) { logger.error() << "could not to initialize engine\n" << err.what(); } #ifdef NDEBUG diff --git a/src/maths/voxmaths.hpp b/src/maths/voxmaths.hpp index 07b42e221..feeb9c8f3 100644 --- a/src/maths/voxmaths.hpp +++ b/src/maths/voxmaths.hpp @@ -1,7 +1,5 @@ #pragma once -#include "typedefs.hpp" - inline constexpr int floordiv(int a, int b) { if (a < 0 && a % b) { return (a / b) - 1; @@ -9,6 +7,23 @@ inline constexpr int floordiv(int a, int b) { return a / b; } +inline constexpr bool is_pot(int a) { + return (a > 0) && ((a & (a - 1)) == 0); +} + +inline constexpr unsigned floorlog2(unsigned x) { + return x == 1 ? 0 : 1 + floorlog2(x >> 1); +} + +template +inline constexpr int floordiv(int a) { + if constexpr (is_pot(b)) { + return a >> floorlog2(b); + } else { + return floordiv(a, b); + } +} + inline constexpr int ceildiv(int a, int b) { if (a > 0 && a % b) { return a / b + 1; diff --git a/src/objects/Entities.cpp b/src/objects/Entities.cpp index 614adf417..79c0ba668 100644 --- a/src/objects/Entities.cpp +++ b/src/objects/Entities.cpp @@ -556,10 +556,6 @@ void Entities::render( float delta, bool pause ) { - if (!pause) { - scripting::on_entities_render(delta); - } - auto view = registry.view(); for (auto [entity, transform, skeleton] : view.each()) { if (transform.dirty) { diff --git a/src/objects/Players.cpp b/src/objects/Players.cpp index 900319917..b174d08aa 100644 --- a/src/objects/Players.cpp +++ b/src/objects/Players.cpp @@ -7,7 +7,7 @@ Players::Players(Level* level) : level(level) {} -void Players::addPlayer(std::unique_ptr player) { +void Players::add(std::unique_ptr player) { players[player->getId()] = std::move(player); } @@ -30,7 +30,7 @@ Player* Players::create() { 0 ); auto player = playerPtr.get(); - addPlayer(std::move(playerPtr)); + add(std::move(playerPtr)); level->inventories->store(player->getInventory()); return player; @@ -62,7 +62,7 @@ void Players::deserialize(const dv::value& src) { ); auto player = playerPtr.get(); player->deserialize(playerMap); - addPlayer(std::move(playerPtr)); + add(std::move(playerPtr)); auto& inventory = player->getInventory(); // invalid inventory id pre 0.25 if (inventory->getId() == 0) { diff --git a/src/objects/Players.hpp b/src/objects/Players.hpp index f66d181f9..a7bda5246 100644 --- a/src/objects/Players.hpp +++ b/src/objects/Players.hpp @@ -17,7 +17,7 @@ class Players : public Serializable { Level* level; std::unordered_map> players; - void addPlayer(std::unique_ptr player); + void add(std::unique_ptr player); public: Players(Level* level); diff --git a/src/util/AreaMap2D.hpp b/src/util/AreaMap2D.hpp index 1a6c97ceb..b7eda1fb7 100644 --- a/src/util/AreaMap2D.hpp +++ b/src/util/AreaMap2D.hpp @@ -27,7 +27,7 @@ namespace util { std::fill(secondBuffer.begin(), secondBuffer.end(), T{}); for (TCoord y = 0; y < sizeY; y++) { for (TCoord x = 0; x < sizeX; x++) { - auto& value = firstBuffer[y * sizeX + x]; + auto value = std::move(firstBuffer[y * sizeX + x]); auto nx = x - dx; auto ny = y - dy; if (value == T{}) { @@ -40,7 +40,7 @@ namespace util { valuesCount--; continue; } - secondBuffer[ny * sizeX + nx] = value; + secondBuffer[ny * sizeX + nx] = std::move(value); } } std::swap(firstBuffer, secondBuffer); diff --git a/src/util/ArgsReader.hpp b/src/util/ArgsReader.hpp new file mode 100644 index 000000000..90d159f50 --- /dev/null +++ b/src/util/ArgsReader.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#include + +namespace util { + class ArgsReader { + const char* last = ""; + char** argv; + int argc; + int pos = 0; + public: + ArgsReader(int argc, char** argv) : argv(argv), argc(argc) { + } + + void skip() { + pos++; + } + + bool hasNext() const { + return pos < argc && std::strlen(argv[pos]); + } + + bool isKeywordArg() const { + return last[0] == '-'; + } + + std::string next() { + if (pos >= argc) { + throw std::runtime_error("unexpected end"); + } + last = argv[pos]; + return argv[pos++]; + } + }; +} diff --git a/src/util/ObjectsKeeper.hpp b/src/util/ObjectsKeeper.hpp index 000c4fc10..74a5a36b4 100644 --- a/src/util/ObjectsKeeper.hpp +++ b/src/util/ObjectsKeeper.hpp @@ -14,5 +14,9 @@ namespace util { virtual void keepAlive(std::shared_ptr ptr) { ptrs.push_back(ptr); } + + virtual void clearKeepedObjects() { + ptrs.clear(); + } }; } diff --git a/src/util/command_line.cpp b/src/util/command_line.cpp index 99501c12f..768c6260e 100644 --- a/src/util/command_line.cpp +++ b/src/util/command_line.cpp @@ -1,81 +1,50 @@ #include "command_line.hpp" -#include #include #include -#include -#include #include "files/engine_paths.hpp" +#include "util/ArgsReader.hpp" +#include "engine.hpp" namespace fs = std::filesystem; -class ArgsReader { - const char* last = ""; - char** argv; - int argc; - int pos = 0; -public: - ArgsReader(int argc, char** argv) : argv(argv), argc(argc) { - } - - void skip() { - pos++; - } - - bool hasNext() const { - return pos < argc && strlen(argv[pos]); - } - - bool isKeywordArg() const { - return last[0] == '-'; - } - - std::string next() { - if (pos >= argc) { - throw std::runtime_error("unexpected end"); - } - last = argv[pos]; - return argv[pos++]; - } -}; - -bool perform_keyword( - ArgsReader& reader, const std::string& keyword, EnginePaths& paths +static bool perform_keyword( + util::ArgsReader& reader, const std::string& keyword, CoreParameters& params ) { if (keyword == "--res") { auto token = reader.next(); - if (!fs::is_directory(fs::path(token))) { - throw std::runtime_error(token + " is not a directory"); - } - paths.setResourcesFolder(fs::path(token)); - std::cout << "resources folder: " << token << std::endl; + params.resFolder = fs::u8path(token); } else if (keyword == "--dir") { auto token = reader.next(); - if (!fs::is_directory(fs::path(token))) { - fs::create_directories(fs::path(token)); - } - paths.setUserFilesFolder(fs::path(token)); - std::cout << "userfiles folder: " << token << std::endl; + params.userFolder = fs::u8path(token); } else if (keyword == "--help" || keyword == "-h") { - std::cout << "VoxelEngine command-line arguments:" << std::endl; - std::cout << " --res [path] - set resources directory" << std::endl; - std::cout << " --dir [path] - set userfiles directory" << std::endl; + std::cout << "VoxelEngine command-line arguments:\n"; + std::cout << " --help - show help\n"; + std::cout << " --res - set resources directory\n"; + std::cout << " --dir - set userfiles directory\n"; + std::cout << " --headless - run in headless mode\n"; + std::cout << " --test - test script file\n"; + std::cout << std::endl; return false; + } else if (keyword == "--headless") { + params.headless = true; + } else if (keyword == "--test") { + auto token = reader.next(); + params.testFile = fs::u8path(token); } else { - std::cerr << "unknown argument " << keyword << std::endl; - return false; + throw std::runtime_error("unknown argument " + keyword); } return true; } -bool parse_cmdline(int argc, char** argv, EnginePaths& paths) { - ArgsReader reader(argc, argv); +bool parse_cmdline(int argc, char** argv, CoreParameters& params) { + util::ArgsReader reader(argc, argv); reader.skip(); while (reader.hasNext()) { std::string token = reader.next(); if (reader.isKeywordArg()) { - if (!perform_keyword(reader, token, paths)) { + if (!perform_keyword(reader, token, params)) { return false; } } else { diff --git a/src/util/command_line.hpp b/src/util/command_line.hpp index 9715c52df..ebb6741d8 100644 --- a/src/util/command_line.hpp +++ b/src/util/command_line.hpp @@ -1,6 +1,6 @@ #pragma once -class EnginePaths; +struct CoreParameters; /// @return false if engine start can -bool parse_cmdline(int argc, char** argv, EnginePaths& paths); +bool parse_cmdline(int argc, char** argv, CoreParameters& params); diff --git a/src/util/platform.cpp b/src/util/platform.cpp index d4d399250..061c78e28 100644 --- a/src/util/platform.cpp +++ b/src/util/platform.cpp @@ -57,7 +57,14 @@ void platform::sleep(size_t millis) { // Reset the timer resolution back to the system default timeEndPeriod(periodMin); } -#else + +int platform::get_process_id() { + return GetCurrentProcessId(); +} + +#else // _WIN32 + +#include void platform::configure_encoding() { } @@ -74,7 +81,11 @@ std::string platform::detect_locale() { void platform::sleep(size_t millis) { std::this_thread::sleep_for(std::chrono::milliseconds(millis)); } -#endif + +int platform::get_process_id() { + return getpid(); +} +#endif // _WIN32 void platform::open_folder(const std::filesystem::path& folder) { if (!std::filesystem::is_directory(folder)) { diff --git a/src/util/platform.hpp b/src/util/platform.hpp index 1283bcfea..6793411ed 100644 --- a/src/util/platform.hpp +++ b/src/util/platform.hpp @@ -12,4 +12,5 @@ namespace platform { void open_folder(const std::filesystem::path& folder); /// Makes the current thread sleep for the specified amount of milliseconds. void sleep(size_t millis); + int get_process_id(); } diff --git a/src/voxels/Chunks.cpp b/src/voxels/Chunks.cpp index 943249647..390ac16c6 100644 --- a/src/voxels/Chunks.cpp +++ b/src/voxels/Chunks.cpp @@ -9,7 +9,6 @@ #include "data/StructLayout.hpp" #include "coders/byte_utils.hpp" -#include "coders/json.hpp" #include "content/Content.hpp" #include "files/WorldFiles.hpp" #include "graphics/core/Mesh.hpp" @@ -39,7 +38,6 @@ Chunks::Chunks( worldFiles(wfile) { areaMap.setCenter(ox-w/2, oz-d/2); areaMap.setOutCallback([this](int, int, const auto& chunk) { - save(chunk.get()); this->level->events->trigger(EVT_CHUNK_HIDDEN, chunk.get()); }); } @@ -48,13 +46,13 @@ voxel* Chunks::get(int32_t x, int32_t y, int32_t z) const { if (y < 0 || y >= CHUNK_H) { return nullptr; } - int cx = floordiv(x, CHUNK_W); - int cz = floordiv(z, CHUNK_D); + int cx = floordiv(x); + int cz = floordiv(z); auto ptr = areaMap.getIf(cx, cz); if (ptr == nullptr) { return nullptr; } - Chunk* chunk = ptr->get(); // not thread safe + Chunk* chunk = ptr->get(); if (chunk == nullptr) { return nullptr; } @@ -107,27 +105,27 @@ const AABB* Chunks::isObstacleAt(float x, float y, float z) const { bool Chunks::isSolidBlock(int32_t x, int32_t y, int32_t z) { voxel* v = get(x, y, z); if (v == nullptr) return false; - return indices->blocks.get(v->id)->rt.solid; //-V522 + return indices->blocks.require(v->id).rt.solid; } bool Chunks::isReplaceableBlock(int32_t x, int32_t y, int32_t z) { voxel* v = get(x, y, z); if (v == nullptr) return false; - return indices->blocks.get(v->id)->replaceable; //-V522 + return indices->blocks.require(v->id).replaceable; } bool Chunks::isObstacleBlock(int32_t x, int32_t y, int32_t z) { voxel* v = get(x, y, z); if (v == nullptr) return false; - return indices->blocks.get(v->id)->obstacle; //-V522 + return indices->blocks.require(v->id).obstacle; } ubyte Chunks::getLight(int32_t x, int32_t y, int32_t z, int channel) const { if (y < 0 || y >= CHUNK_H) { return 0; } - int cx = floordiv(x, CHUNK_W); - int cz = floordiv(z, CHUNK_D); + int cx = floordiv(x); + int cz = floordiv(z); auto ptr = areaMap.getIf(cx, cz); if (ptr == nullptr) { @@ -146,8 +144,8 @@ light_t Chunks::getLight(int32_t x, int32_t y, int32_t z) const { if (y < 0 || y >= CHUNK_H) { return 0; } - int cx = floordiv(x, CHUNK_W); - int cz = floordiv(z, CHUNK_D); + int cx = floordiv(x); + int cz = floordiv(z); auto ptr = areaMap.getIf(cx, cz); if (ptr == nullptr) { @@ -166,8 +164,8 @@ Chunk* Chunks::getChunkByVoxel(int32_t x, int32_t y, int32_t z) const { if (y < 0 || y >= CHUNK_H) { return nullptr; } - int cx = floordiv(x, CHUNK_W); - int cz = floordiv(z, CHUNK_D); + int cx = floordiv(x); + int cz = floordiv(z); if (auto ptr = areaMap.getIf(cx, cz)) { return ptr->get(); } @@ -369,8 +367,8 @@ void Chunks::set( if (y < 0 || y >= CHUNK_H) { return; } - int cx = floordiv(x, CHUNK_W); - int cz = floordiv(z, CHUNK_D); + int cx = floordiv(x); + int cz = floordiv(z); auto ptr = areaMap.getIf(cx, cz); if (ptr == nullptr) { return; @@ -673,7 +671,11 @@ void Chunks::resize(uint32_t newW, uint32_t newD) { } bool Chunks::putChunk(const std::shared_ptr& chunk) { - return areaMap.set(chunk->x, chunk->z, chunk); + if (areaMap.set(chunk->x, chunk->z, chunk)) { + level->events->trigger(LevelEventType::EVT_CHUNK_SHOWN, chunk.get()); + return true; + } + return false; } // reduce nesting on next modification @@ -692,11 +694,11 @@ void Chunks::getVoxels(VoxelsVolume* volume, bool backlight) const { int h = volume->getH(); int d = volume->getD(); - int scx = floordiv(x, CHUNK_W); - int scz = floordiv(z, CHUNK_D); + int scx = floordiv(x); + int scz = floordiv(z); - int ecx = floordiv(x + w, CHUNK_W); - int ecz = floordiv(z + d, CHUNK_D); + int ecx = floordiv(x + w); + int ecz = floordiv(z + d); int cw = ecx - scx + 1; int cd = ecz - scz + 1; @@ -768,35 +770,3 @@ void Chunks::getVoxels(VoxelsVolume* volume, bool backlight) const { void Chunks::saveAndClear() { areaMap.clear(); } - -void Chunks::save(Chunk* chunk) { - if (chunk != nullptr) { - AABB aabb( - glm::vec3(chunk->x * CHUNK_W, -INFINITY, chunk->z * CHUNK_D), - glm::vec3( - (chunk->x + 1) * CHUNK_W, INFINITY, (chunk->z + 1) * CHUNK_D - ) - ); - auto entities = level->entities->getAllInside(aabb); - auto root = dv::object(); - root["data"] = level->entities->serialize(entities); - if (!entities.empty()) { - level->entities->despawn(std::move(entities)); - chunk->flags.entities = true; - } - worldFiles->getRegions().put( - chunk, - chunk->flags.entities ? json::to_binary(root, true) - : std::vector() - ); - } -} - -void Chunks::saveAll() { - const auto& chunks = areaMap.getBuffer(); - for (size_t i = 0; i < areaMap.area(); i++) { - if (auto& chunk = chunks[i]) { - save(chunk.get()); - } - } -} diff --git a/src/voxels/Chunks.hpp b/src/voxels/Chunks.hpp index 01648301a..fd33eb9a9 100644 --- a/src/voxels/Chunks.hpp +++ b/src/voxels/Chunks.hpp @@ -124,8 +124,6 @@ class Chunks { void resize(uint32_t newW, uint32_t newD); void saveAndClear(); - void save(Chunk* chunk); - void saveAll(); const std::vector>& getChunks() const { return areaMap.getBuffer(); diff --git a/src/voxels/ChunksStorage.cpp b/src/voxels/ChunksStorage.cpp deleted file mode 100644 index 272332fb3..000000000 --- a/src/voxels/ChunksStorage.cpp +++ /dev/null @@ -1,101 +0,0 @@ -#include "ChunksStorage.hpp" - -#include - -#include "content/Content.hpp" -#include "debug/Logger.hpp" -#include "files/WorldFiles.hpp" -#include "items/Inventories.hpp" -#include "lighting/Lightmap.hpp" -#include "maths/voxmaths.hpp" -#include "objects/Entities.hpp" -#include "typedefs.hpp" -#include "world/Level.hpp" -#include "world/World.hpp" -#include "Block.hpp" -#include "Chunk.hpp" - -static debug::Logger logger("chunks-storage"); - -ChunksStorage::ChunksStorage(Level* level) : level(level) { -} - -void ChunksStorage::store(const std::shared_ptr& chunk) { - chunksMap[glm::ivec2(chunk->x, chunk->z)] = chunk; -} - -std::shared_ptr ChunksStorage::get(int x, int z) const { - auto found = chunksMap.find(glm::ivec2(x, z)); - if (found == chunksMap.end()) { - return nullptr; - } - return found->second; -} - -void ChunksStorage::remove(int x, int z) { - auto found = chunksMap.find(glm::ivec2(x, z)); - if (found != chunksMap.end()) { - chunksMap.erase(found->first); - } -} - -static void check_voxels(const ContentIndices& indices, Chunk* chunk) { - for (size_t i = 0; i < CHUNK_VOL; i++) { - blockid_t id = chunk->voxels[i].id; - if (indices.blocks.get(id) == nullptr) { - auto logline = logger.error(); - logline << "corruped block detected at " << i << " of chunk "; - logline << chunk->x << "x" << chunk->z; - logline << " -> " << id; - chunk->voxels[i].id = BLOCK_AIR; - } - } -} - -std::shared_ptr ChunksStorage::create(int x, int z) { - World* world = level->getWorld(); - auto& regions = world->wfile.get()->getRegions(); - - auto chunk = std::make_shared(x, z); - store(chunk); - if (auto data = regions.getVoxels(chunk->x, chunk->z)) { - const auto& indices = *level->content->getIndices(); - - chunk->decode(data.get()); - check_voxels(indices, chunk.get()); - - auto invs = regions.fetchInventories(chunk->x, chunk->z); - auto iterator = invs.begin(); - while (iterator != invs.end()) { - uint index = iterator->first; - const auto& def = indices.blocks.require(chunk->voxels[index].id); - if (def.inventorySize == 0) { - iterator = invs.erase(iterator); - continue; - } - auto& inventory = iterator->second; - if (def.inventorySize != inventory->size()) { - inventory->resize(def.inventorySize); - } - ++iterator; - } - chunk->setBlockInventories(std::move(invs)); - - auto entitiesData = regions.fetchEntities(chunk->x, chunk->z); - if (entitiesData.getType() == dv::value_type::object) { - level->entities->loadEntities(std::move(entitiesData)); - chunk->flags.entities = true; - } - - chunk->flags.loaded = true; - for (auto& entry : chunk->inventories) { - level->inventories->store(entry.second); - } - } - if (auto lights = regions.getLights(chunk->x, chunk->z)) { - chunk->lightmap.set(lights.get()); - chunk->flags.loadedLights = true; - } - chunk->blocksMetadata = regions.getBlocksData(chunk->x, chunk->z); - return chunk; -} diff --git a/src/voxels/ChunksStorage.hpp b/src/voxels/ChunksStorage.hpp deleted file mode 100644 index 35fff6c1d..000000000 --- a/src/voxels/ChunksStorage.hpp +++ /dev/null @@ -1,26 +0,0 @@ -#pragma once - -#include -#include - -#include "typedefs.hpp" -#include "voxel.hpp" - -#define GLM_ENABLE_EXPERIMENTAL -#include - -class Chunk; -class Level; - -class ChunksStorage { - Level* level; - std::unordered_map> chunksMap; -public: - ChunksStorage(Level* level); - ~ChunksStorage() = default; - - std::shared_ptr get(int x, int z) const; - void store(const std::shared_ptr& chunk); - void remove(int x, int y); - std::shared_ptr create(int x, int z); -}; diff --git a/src/voxels/GlobalChunks.cpp b/src/voxels/GlobalChunks.cpp new file mode 100644 index 000000000..379ca3288 --- /dev/null +++ b/src/voxels/GlobalChunks.cpp @@ -0,0 +1,211 @@ +#include "GlobalChunks.hpp" + +#include + +#include "content/Content.hpp" +#include "coders/json.hpp" +#include "debug/Logger.hpp" +#include "files/WorldFiles.hpp" +#include "items/Inventories.hpp" +#include "lighting/Lightmap.hpp" +#include "maths/voxmaths.hpp" +#include "objects/Entities.hpp" +#include "typedefs.hpp" +#include "world/Level.hpp" +#include "world/World.hpp" +#include "Block.hpp" +#include "Chunk.hpp" + +inline long long keyfrom(int x, int z) { + union { + int pos[2]; + long long key; + } ekey; + ekey.pos[0] = x; + ekey.pos[1] = z; + return ekey.key; +} + +static debug::Logger logger("chunks-storage"); + +GlobalChunks::GlobalChunks(Level* level) : level(level) { +} + +std::shared_ptr GlobalChunks::fetch(int x, int z) { + const auto& found = chunksMap.find(keyfrom(x, z)); + if (found == chunksMap.end()) { + return nullptr; + } + return found->second; +} + +static void check_voxels(const ContentIndices& indices, Chunk& chunk) { + bool corrupted = false; + blockid_t defsCount = indices.blocks.count(); + for (size_t i = 0; i < CHUNK_VOL; i++) { + blockid_t id = chunk.voxels[i].id; + if (id >= defsCount) { + if (!corrupted) { +#ifdef NDEBUG + // release + auto logline = logger.error(); + logline << "corruped blocks detected at " << i << " of chunk "; + logline << chunk.x << "x" << chunk.z; + logline << " -> " << id; + corrupted = true; +#else + // debug + abort(); +#endif + } + chunk.voxels[i].id = BLOCK_AIR; + } + } +} + +void GlobalChunks::erase(int x, int z) { + chunksMap.erase(keyfrom(x, z)); +} + +std::shared_ptr GlobalChunks::create(int x, int z) { + const auto& found = chunksMap.find(keyfrom(x, z)); + if (found != chunksMap.end()) { + return found->second; + } + + auto chunk = std::make_shared(x, z); + chunksMap[keyfrom(x, z)] = chunk; + + World* world = level->getWorld(); + auto& regions = world->wfile.get()->getRegions(); + + if (auto data = regions.getVoxels(chunk->x, chunk->z)) { + const auto& indices = *level->content->getIndices(); + + chunk->decode(data.get()); + check_voxels(indices, *chunk); + auto invs = regions.fetchInventories(chunk->x, chunk->z); + auto iterator = invs.begin(); + while (iterator != invs.end()) { + uint index = iterator->first; + const auto& def = indices.blocks.require(chunk->voxels[index].id); + if (def.inventorySize == 0) { + iterator = invs.erase(iterator); + continue; + } + auto& inventory = iterator->second; + if (def.inventorySize != inventory->size()) { + inventory->resize(def.inventorySize); + } + ++iterator; + } + chunk->setBlockInventories(std::move(invs)); + + auto entitiesData = regions.fetchEntities(chunk->x, chunk->z); + if (entitiesData.getType() == dv::value_type::object) { + level->entities->loadEntities(std::move(entitiesData)); + chunk->flags.entities = true; + } + + chunk->flags.loaded = true; + for (auto& entry : chunk->inventories) { + level->inventories->store(entry.second); + } + } + if (auto lights = regions.getLights(chunk->x, chunk->z)) { + chunk->lightmap.set(lights.get()); + chunk->flags.loadedLights = true; + } + chunk->blocksMetadata = regions.getBlocksData(chunk->x, chunk->z); + return chunk; +} + +void GlobalChunks::pinChunk(std::shared_ptr chunk) { + pinnedChunks[{chunk->x, chunk->z}] = std::move(chunk); +} + +void GlobalChunks::unpinChunk(int x, int z) { + pinnedChunks.erase({x, z}); +} + +size_t GlobalChunks::size() const { + return chunksMap.size(); +} + +voxel* GlobalChunks::get(int x, int y, int z) const { + if (y < 0 || y >= CHUNK_H) { + return nullptr; + } + + int cx = floordiv(x); + int cz = floordiv(z); + + const auto& found = chunksMap.find(keyfrom(cx, cz)); + if (found == chunksMap.end()) { + return nullptr; + } + const auto& chunk = found->second; + int lx = x - cx * CHUNK_W; + int lz = z - cz * CHUNK_D; + return &chunk->voxels[(y * CHUNK_D + lz) * CHUNK_W + lx]; +} + +void GlobalChunks::incref(Chunk* chunk) { + auto key = reinterpret_cast(chunk); + const auto& found = refCounters.find(key); + if (found == refCounters.end()) { + refCounters[key] = 1; + return; + } + found->second++; +} + +void GlobalChunks::decref(Chunk* chunk) { + auto key = reinterpret_cast(chunk); + const auto& found = refCounters.find(key); + if (found == refCounters.end()) { + abort(); + } + if (--found->second == 0) { + union { + int pos[2]; + long long key; + } ekey; + ekey.pos[0] = chunk->x; + ekey.pos[1] = chunk->z; + + save(chunk); + chunksMap.erase(ekey.key); + refCounters.erase(found); + } +} + +void GlobalChunks::save(Chunk* chunk) { + if (chunk == nullptr) { + return; + } + AABB aabb( + glm::vec3(chunk->x * CHUNK_W, -INFINITY, chunk->z * CHUNK_D), + glm::vec3( + (chunk->x + 1) * CHUNK_W, INFINITY, (chunk->z + 1) * CHUNK_D + ) + ); + auto entities = level->entities->getAllInside(aabb); + auto root = dv::object(); + root["data"] = level->entities->serialize(entities); + if (!entities.empty()) { + level->entities->despawn(std::move(entities)); + chunk->flags.entities = true; + } + level->getWorld()->wfile->getRegions().put( + chunk, + chunk->flags.entities ? json::to_binary(root, true) + : std::vector() + ); +} + +void GlobalChunks::saveAll() { + for (const auto& [_, chunk] : chunksMap) { + save(chunk.get()); + } +} diff --git a/src/voxels/GlobalChunks.hpp b/src/voxels/GlobalChunks.hpp new file mode 100644 index 000000000..b21e15e6b --- /dev/null +++ b/src/voxels/GlobalChunks.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include +#include + +#define GLM_ENABLE_EXPERIMENTAL +#include +#include + +#include "voxel.hpp" + +class Chunk; +class Level; + +class GlobalChunks { + Level* level; + std::unordered_map> chunksMap; + std::unordered_map> pinnedChunks; + std::unordered_map refCounters; +public: + GlobalChunks(Level* level); + ~GlobalChunks() = default; + + std::shared_ptr fetch(int x, int z); + std::shared_ptr create(int x, int z); + + void pinChunk(std::shared_ptr chunk); + void unpinChunk(int x, int z); + + voxel* get(int x, int y, int z) const; + + size_t size() const; + + void incref(Chunk* chunk); + void decref(Chunk* chunk); + + void erase(int x, int z); + + void save(Chunk* chunk); + void saveAll(); +}; diff --git a/src/world/Level.cpp b/src/world/Level.cpp index ed370d084..99f47d866 100644 --- a/src/world/Level.cpp +++ b/src/world/Level.cpp @@ -13,7 +13,7 @@ #include "settings.hpp" #include "voxels/Chunk.hpp" #include "voxels/Chunks.hpp" -#include "voxels/ChunksStorage.hpp" +#include "voxels/GlobalChunks.hpp" #include "window/Camera.hpp" #include "LevelEvents.hpp" #include "World.hpp" @@ -25,7 +25,7 @@ Level::Level( ) : world(std::move(worldPtr)), content(content), - chunksStorage(std::make_unique(this)), + chunksStorage(std::make_unique(this)), physics(std::make_unique(glm::vec3(0, -22.6f, 0))), events(std::make_unique()), entities(std::make_unique(this)), @@ -54,17 +54,21 @@ Level::Level( entities->setNextID(worldInfo.nextEntityId); } + events->listen(LevelEventType::EVT_CHUNK_SHOWN, [this](LevelEventType, Chunk* chunk) { + chunksStorage->incref(chunk); + }); + events->listen(LevelEventType::EVT_CHUNK_HIDDEN, [this](LevelEventType, Chunk* chunk) { + chunksStorage->decref(chunk); + }); + uint matrixSize = (settings.chunks.loadDistance.get() + settings.chunks.padding.get()) * 2; chunks = std::make_unique( matrixSize, matrixSize, 0, 0, world->wfile.get(), this ); - lighting = std::make_unique(content, chunks.get()); - events->listen(EVT_CHUNK_HIDDEN, [this](lvl_event_type, Chunk* chunk) { - this->chunksStorage->remove(chunk->x, chunk->z); - }); + lighting = std::make_unique(content, chunks.get()); inventories = std::make_unique(*this); } diff --git a/src/world/Level.hpp b/src/world/Level.hpp index 1f3ef183f..632746720 100644 --- a/src/world/Level.hpp +++ b/src/world/Level.hpp @@ -15,7 +15,7 @@ class Inventories; class LevelEvents; class Lighting; class PhysicsSolver; -class ChunksStorage; +class GlobalChunks; class Camera; class Players; struct EngineSettings; @@ -27,7 +27,7 @@ class Level { const Content* const content; std::unique_ptr chunks; - std::unique_ptr chunksStorage; + std::unique_ptr chunksStorage; std::unique_ptr inventories; std::unique_ptr physics; diff --git a/src/world/LevelEvents.cpp b/src/world/LevelEvents.cpp index 00017cc66..cb98cb9dd 100644 --- a/src/world/LevelEvents.cpp +++ b/src/world/LevelEvents.cpp @@ -4,14 +4,14 @@ using std::vector; -void LevelEvents::listen(lvl_event_type type, const chunk_event_func& func) { +void LevelEvents::listen(LevelEventType type, const ChunkEventFunc& func) { auto& callbacks = chunk_callbacks[type]; callbacks.push_back(func); } -void LevelEvents::trigger(lvl_event_type type, Chunk* chunk) { +void LevelEvents::trigger(LevelEventType type, Chunk* chunk) { const auto& callbacks = chunk_callbacks[type]; - for (const chunk_event_func& func : callbacks) { + for (const ChunkEventFunc& func : callbacks) { func(type, chunk); } } diff --git a/src/world/LevelEvents.hpp b/src/world/LevelEvents.hpp index d0518b5ca..f7bbaa7f6 100644 --- a/src/world/LevelEvents.hpp +++ b/src/world/LevelEvents.hpp @@ -6,16 +6,17 @@ class Chunk; -enum lvl_event_type { +enum LevelEventType { + EVT_CHUNK_SHOWN, EVT_CHUNK_HIDDEN, }; -using chunk_event_func = std::function; +using ChunkEventFunc = std::function; class LevelEvents { - std::unordered_map> + std::unordered_map> chunk_callbacks; public: - void listen(lvl_event_type type, const chunk_event_func& func); - void trigger(lvl_event_type type, Chunk* chunk); + void listen(LevelEventType type, const ChunkEventFunc& func); + void trigger(LevelEventType type, Chunk* chunk); }; diff --git a/src/world/World.cpp b/src/world/World.cpp index 4eb2631a9..eb8c33bf7 100644 --- a/src/world/World.cpp +++ b/src/world/World.cpp @@ -15,7 +15,7 @@ #include "settings.hpp" #include "voxels/Chunk.hpp" #include "voxels/Chunks.hpp" -#include "voxels/ChunksStorage.hpp" +#include "voxels/GlobalChunks.hpp" #include "world/generator/WorldGenerator.hpp" #include "world/generator/GeneratorDef.hpp" #include "Level.hpp" @@ -66,7 +66,7 @@ void World::writeResources(const Content* content) { void World::write(Level* level) { const Content* content = level->content; - level->chunks->saveAll(); + level->chunksStorage->saveAll(); info.nextEntityId = level->entities->peekNextID(); wfile->write(this, content); @@ -95,9 +95,9 @@ std::unique_ptr World::create( content, packs ); - auto level = std::make_unique(std::move(world), content, settings); - level->players->create(); - return level; + logger.info() << "created world '" << name << "' (" << directory.u8string() << ")"; + logger.info() << "world seed: " << seed << " generator: " << generator; + return std::make_unique(std::move(world), content, settings); } std::unique_ptr World::load( @@ -133,8 +133,10 @@ std::unique_ptr World::load( auto playerRoot = files::read_json(file); level->players->deserialize(playerRoot); - if (!playerRoot["players"][0].has("id")) { - level->getWorld()->getInfo().nextPlayerId++; + if (!playerRoot["players"].empty()) { + if (!playerRoot["players"][0].has("id")) { + level->getWorld()->getInfo().nextPlayerId++; + } } } return level; diff --git a/src/world/generator/VoxelFragment.cpp b/src/world/generator/VoxelFragment.cpp index f36824b0b..2d7ab9239 100644 --- a/src/world/generator/VoxelFragment.cpp +++ b/src/world/generator/VoxelFragment.cpp @@ -8,7 +8,7 @@ #include "content/Content.hpp" #include "voxels/Chunks.hpp" #include "voxels/Block.hpp" -#include "voxels/ChunksStorage.hpp" +#include "voxels/GlobalChunks.hpp" #include "voxels/VoxelsVolume.hpp" #include "world/Level.hpp" #include "core_defs.hpp" diff --git a/vctest/CMakeLists.txt b/vctest/CMakeLists.txt new file mode 100644 index 000000000..6c1a3843c --- /dev/null +++ b/vctest/CMakeLists.txt @@ -0,0 +1,31 @@ +project(vctest) + +set(CMAKE_CXX_STANDARD 17) + +file(GLOB_RECURSE SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp) + +add_executable(${PROJECT_NAME} ${SOURCES}) + +if(MSVC) + if(NOT CMAKE_BUILD_TYPE) + 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) + endif() + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") +else() + target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra + -Wformat-nonliteral -Wcast-align + -Wpointer-arith -Wundef + -Wwrite-strings -Wno-unused-parameter) +endif() + +if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -no-pie -lstdc++fs") +endif() + +target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../src ${CMAKE_DL_LIBS}) diff --git a/vctest/main.cpp b/vctest/main.cpp new file mode 100644 index 000000000..7757041e1 --- /dev/null +++ b/vctest/main.cpp @@ -0,0 +1,221 @@ +#include +#include +#include +#include +#include +#include + +#include "util/ArgsReader.hpp" + +namespace fs = std::filesystem; + +inline fs::path TESTING_DIR = fs::u8path(".vctest"); + +struct Config { + fs::path executable; + fs::path directory; + fs::path resDir {"res"}; + fs::path workingDir {"."}; + bool outputAlways = false; +}; + +static bool perform_keyword( + util::ArgsReader& reader, const std::string& keyword, Config& config +) { + if (keyword == "--help" || keyword == "-h") { + std::cout << "Options\n\n"; + std::cout << " --help, -h = show help\n"; + std::cout << " --exe , -e = VoxelCore executable path\n"; + std::cout << " --tests , -d = tests directory path\n"; + std::cout << " --res , -r = 'res' directory path\n"; + std::cout << " --working-dir , -w = user directory path\n"; + std::cout << " --output-always = always show tests output\n"; + std::cout << std::endl; + return false; + } else if (keyword == "--exe" || keyword == "-e") { + config.executable = fs::path(reader.next()); + } else if (keyword == "--tests" || keyword == "-d") { + config.directory = fs::path(reader.next()); + } else if (keyword == "--res" || keyword == "-r") { + config.resDir = fs::path(reader.next()); + } else if (keyword == "--user" || keyword == "-u") { + config.workingDir = fs::path(reader.next()); + } else if (keyword == "--output-always") { + config.outputAlways = true; + } else { + std::cerr << "unknown argument " << keyword << std::endl; + return false; + } + return true; +} + +static bool parse_cmdline(int argc, char** argv, Config& config) { + util::ArgsReader reader(argc, argv); + while (reader.hasNext()) { + std::string token = reader.next(); + if (reader.isKeywordArg()) { + if (!perform_keyword(reader, token, config)) { + return false; + } + } + } + return true; +} + +static bool check_dir(const fs::path& dir) { + if (!fs::is_directory(dir)) { + std::cerr << dir << " is not a directory" << std::endl; + return false; + } + return true; +} + +static void print_separator(std::ostream& stream) { + for (int i = 0; i < 32; i++) { + stream << "="; + } + stream << "\n"; +} + +static bool check_config(const Config& config) { + if (!fs::exists(config.executable)) { + std::cerr << "file " << config.executable << " not found" << std::endl; + return true; + } + if (!check_dir(config.directory)) { + return true; + } + if (!check_dir(config.resDir)) { + return true; + } + if (!check_dir(config.workingDir)) { + return true; + } + return false; +} + +static void dump_config(const Config& config) { + std::cout << "paths:\n"; + std::cout << " VoxelCore executable = " << fs::canonical(config.executable).string() << "\n"; + std::cout << " Tests directory = " << fs::canonical(config.directory).string() << "\n"; + std::cout << " Resources directory = " << fs::canonical(config.resDir).string() << "\n"; + std::cout << " Working directory = " << fs::canonical(config.workingDir).string(); + std::cout << std::endl; +} + +static void cleanup(const fs::path& dir) { + std::cout << "cleaning up " << dir << std::endl; + fs::remove_all(dir); +} + +static void setup_working_dir(const fs::path& workingDir) { + auto dir = workingDir / TESTING_DIR; + std::cout << "setting up working directory " << dir << std::endl; + if (fs::is_directory(dir)) { + cleanup(dir); + } + fs::create_directories(dir); +} + +static void display_test_output(const fs::path& path, std::ostream& stream) { + stream << "[OUTPUT]" << std::endl; + if (fs::exists(path)) { + std::ifstream t(path); + stream << t.rdbuf(); + } +} + +static std::string fix_path(std::string s) { + for (char& c : s) { + if (c == '\\') { + c = '/'; + } + } + return s; +} + +static bool run_test(const Config& config, const fs::path& path) { + using std::chrono::duration_cast; + using std::chrono::high_resolution_clock; + using std::chrono::milliseconds; + + auto outputFile = config.workingDir / "output.txt"; + + auto name = path.stem(); + std::stringstream ss; + ss << fs::canonical(config.executable) << " --headless"; + ss << " --test " << fix_path(path.string()); + ss << " --res " << fix_path(config.resDir.string()); + ss << " --dir " << fix_path(config.workingDir.string()); + ss << " >" << fix_path(outputFile.string()) << " 2>&1"; + auto command = ss.str(); + + print_separator(std::cout); + std::cout << "executing test " << name << "\ncommand: " << command << std::endl; + + auto start = high_resolution_clock::now(); + int code = system(command.c_str()); + auto testTime = + duration_cast(high_resolution_clock::now() - start) + .count(); + + if (code) { + display_test_output(outputFile, std::cerr); + std::cerr << "[FAILED] " << name << " in " << testTime << " ms" << std::endl; + fs::remove(outputFile); + return false; + } else { + if (config.outputAlways) { + display_test_output(outputFile, std::cout); + } + std::cout << "[PASSED] " << name << " in " << testTime << " ms" << std::endl; + fs::remove(outputFile); + return true; + } +} + +int main(int argc, char** argv) { + Config config; + try { + if (!parse_cmdline(argc, argv, config)) { + return 0; + } + } catch (const std::runtime_error& err) { + std::cerr << err.what() << std::endl; + throw; + } + if (check_config(config)) { + return 1; + } + dump_config(config); + + std::vector tests; + std::cout << "scanning for tests" << std::endl; + for (const auto& entry : fs::directory_iterator(config.directory)) { + auto path = entry.path(); + if (path.extension().string() != ".lua") { + std::cout << " " << entry.path() << " skipped" << std::endl; + continue; + } + std::cout << " " << entry.path() << " enqueued" << std::endl; + tests.push_back(path); + } + + setup_working_dir(config.workingDir); + config.workingDir /= TESTING_DIR; + + size_t passed = 0; + std::cout << "running " << tests.size() << " test(s)" << std::endl; + for (const auto& path : tests) { + passed += run_test(config, path); + } + print_separator(std::cout); + cleanup(config.workingDir); + std::cout << std::endl; + std::cout << passed << " test(s) passed, " << (tests.size() - passed) + << " test(s) failed" << std::endl; + if (passed < tests.size()) { + return 1; + } + return 0; +}