diff --git a/.github/workflows/appimage.yml b/.github/workflows/appimage.yml index a72489f0b..25ae572fc 100644 --- a/.github/workflows/appimage.yml +++ b/.github/workflows/appimage.yml @@ -4,7 +4,7 @@ on: push: branches: [ "main", "release-**"] pull_request: - branches: [ "main" ] + branches: [ "main", "headless-mode" ] jobs: build-appimage: @@ -24,7 +24,7 @@ jobs: 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 libgtest-dev cmake squashfs-tools tree + 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 diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 3efb909cd..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: diff --git a/.github/workflows/windows-clang.yml b/.github/workflows/windows-clang.yml index 89fc272d1..23544bf64 100644 --- a/.github/workflows/windows-clang.yml +++ b/.github/workflows/windows-clang.yml @@ -4,7 +4,7 @@ on: push: branches: [ "main", "release-**"] pull_request: - branches: [ "main" ] + branches: [ "main", "headless-mode" ] jobs: build-windows: @@ -31,7 +31,7 @@ jobs: mingw-w64-clang-x86_64-cmake mingw-w64-clang-x86_64-make mingw-w64-clang-x86_64-luajit - git tree + git - name: Set up vcpkg shell: msys2 {0} run: | diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index de477e896..d417197c9 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -4,7 +4,7 @@ on: push: branches: [ "main", "release-**"] pull_request: - branches: [ "main" ] + branches: [ "main", "headless-mode" ] jobs: build-windows: diff --git a/dev/tests/chunks.lua b/dev/tests/chunks.lua new file mode 100644 index 000000000..dbc001ba1 --- /dev/null +++ b/dev/tests/chunks.lua @@ -0,0 +1,27 @@ +test.set_setting("chunks.load-distance", 3) +test.set_setting("chunks.load-speed", 1) + +test.reconfig_packs({"base"}, {}) +test.new_world("demo", "2019", "core:default") + +local pid1 = player.create("Xerxes") +assert(player.get_name(pid1) == "Xerxes") + +local pid2 = player.create("Segfault") +assert(player.get_name(pid2) == "Segfault") + +local seed = math.floor(math.random() * 1e6) +print("random seed", seed) +math.randomseed(seed) + +for i=1,25 do + if i % 5 == 0 then + print(tostring(i*4).." % done") + print("chunks loaded", world.count_chunks()) + end + player.set_pos(pid1, math.random() * 100 - 50, 100, math.random() * 100 - 50) + player.set_pos(pid2, math.random() * 200 - 100, 100, math.random() * 200 - 100) + test.tick() +end + +test.close_world(true) diff --git a/dev/tests/example.lua b/dev/tests/example.lua deleted file mode 100644 index c066c1ed4..000000000 --- a/dev/tests/example.lua +++ /dev/null @@ -1,19 +0,0 @@ -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/doc/en/audio.md b/doc/en/audio.md index 9322583fd..cacd71074 100644 --- a/doc/en/audio.md +++ b/doc/en/audio.md @@ -60,7 +60,7 @@ Library **audio** contains available Audio API in Lua scripts. ```lua audio.play_stream( - -- audio file location + -- audio file location (without entry point, but with extension included) name: string, -- audio source world position x: number, y: number, z: number, @@ -79,7 +79,7 @@ Plays streaming audio from the specified file at the specified world position. R ```lua audio.play_stream_2d( - -- audio file location + -- audio file location (without entry point, but with extension included) name: string, -- audio gain (0.0 - 1.0) volume: number @@ -202,4 +202,4 @@ audio.count_speakers() -> integer -- get current number of playing streams audio.count_streams() -> integer -``` \ No newline at end of file +``` diff --git a/doc/en/world-generator.md b/doc/en/world-generator.md index 2112bce21..b0ab265c9 100644 --- a/doc/en/world-generator.md +++ b/doc/en/world-generator.md @@ -52,6 +52,7 @@ The main properties described in the configuration file: - **biomes-bpd** - number of blocks per point of the biome selection parameter map. Default: 4. - **heights-bpd** - number of blocks per point of the height map. Default: 4. - **wide-structs-chunks-radius** - maximum radius for placing 'wide' structures, measured in chunks. +- **heightmap-inputs** - an array of parameter map numbers that will be passed by the inputs table to the height map generation function. ## Global variables diff --git a/doc/ru/audio.md b/doc/ru/audio.md index b560b7392..1fce6c358 100644 --- a/doc/ru/audio.md +++ b/doc/ru/audio.md @@ -61,7 +61,7 @@ ```lua audio.play_stream( - -- путь к аудио-файлу + -- путь к аудио-файлу (без точки входа, но с указанием расширения) name: string, -- позиция источника аудио в мире x: number, y: number, z: number, @@ -80,7 +80,7 @@ audio.play_stream( ```lua audio.play_stream_2d( - -- путь к аудио-файлу + -- путь к аудио-файлу (без точки входа, но с указанием расширения) name: string, -- громкость аудио (от 0.0 до 1.0) volume: number diff --git a/doc/ru/world-generator.md b/doc/ru/world-generator.md index 1523f94ad..5c1c6e6e9 100644 --- a/doc/ru/world-generator.md +++ b/doc/ru/world-generator.md @@ -52,6 +52,7 @@ - **biomes-bpd** - количество блоков на точку карты параметра выбора биомов. По-умолчанию: 4. - **heights-bpd** - количество блоков на точку карты высот. По-умолчанию: 4. - **wide-structs-chunks-radius** - масимальный радиус размещения 'широких' структур, измеряемый в чанках. +- **heightmap-inputs** - массив номеров карт параметров, которые будут переданы таблицей inputs в функцию генерации карты высот. ## Глобальные переменные diff --git a/res/scripts/stdlib.lua b/res/scripts/stdlib.lua index 0298a2b9b..aa56bc315 100644 --- a/res/scripts/stdlib.lua +++ b/res/scripts/stdlib.lua @@ -9,16 +9,42 @@ function sleep(timesec) end end +function tb_frame_tostring(frame) + local s = frame.short_src + if frame.what ~= "C" then + s = s .. ":" .. tostring(frame.currentline) + end + if frame.what == "main" then + s = s .. ": in main chunk" + elseif frame.name then + s = s .. ": in function " .. utf8.escape(frame.name) + end + return s +end + 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.reopen_world = core.reopen_world + test.delete_world = core.delete_world test.reconfig_packs = core.reconfig_packs test.set_setting = core.set_setting test.tick = coroutine.yield + function test.quit() + local tb = debug.get_traceback(1) + local s = "test.quit() traceback:" + for i, frame in ipairs(tb) do + s = s .. "\n\t"..tb_frame_tostring(frame) + end + debug.log(s) + core.quit() + coroutine.yield() + end + function test.sleep_until(predicate, max_ticks) max_ticks = max_ticks or 1e9 local ticks = 0 @@ -382,7 +408,9 @@ end function __vc_stop_coroutine(id) local co = __vc_coroutines[id] if co then - coroutine.close(co) + if coroutine.close then + coroutine.close(co) + end __vc_coroutines[id] = nil end end diff --git a/res/scripts/stdmin.lua b/res/scripts/stdmin.lua index 71fdbda2d..678983609 100644 --- a/res/scripts/stdmin.lua +++ b/res/scripts/stdmin.lua @@ -74,7 +74,7 @@ function table.deep_copy(t) end end - return copied + return setmetatable(copied, getmetatable(t)) end function table.count_pairs(t) diff --git a/src/Mainloop.cpp b/src/Mainloop.cpp index eae4c501c..a12434ef2 100644 --- a/src/Mainloop.cpp +++ b/src/Mainloop.cpp @@ -20,14 +20,14 @@ void Mainloop::run() { // destroy LevelScreen and run quit callbacks engine.setScreen(nullptr); // create and go to menu screen - engine.setScreen(std::make_shared(&engine)); + engine.setScreen(std::make_shared(engine)); } else { - engine.setScreen(std::make_shared(&engine, std::move(level))); + engine.setScreen(std::make_shared(engine, std::move(level))); } }); logger.info() << "starting menu screen"; - engine.setScreen(std::make_shared(&engine)); + engine.setScreen(std::make_shared(engine)); logger.info() << "main loop started"; while (!Window::isShouldClose()){ diff --git a/src/ServerMainloop.cpp b/src/ServerMainloop.cpp new file mode 100644 index 000000000..2c88a06b2 --- /dev/null +++ b/src/ServerMainloop.cpp @@ -0,0 +1,85 @@ +#include "ServerMainloop.hpp" + +#include "logic/scripting/scripting.hpp" +#include "logic/LevelController.hpp" +#include "interfaces/Process.hpp" +#include "debug/Logger.hpp" +#include "world/Level.hpp" +#include "world/World.hpp" +#include "util/platform.hpp" +#include "engine.hpp" + +#include + +using namespace std::chrono; + +static debug::Logger logger("mainloop"); + +inline constexpr int TPS = 20; + +ServerMainloop::ServerMainloop(Engine& engine) : engine(engine) { +} + +ServerMainloop::~ServerMainloop() = default; + +void ServerMainloop::run() { + const auto& coreParams = engine.getCoreParameters(); + auto& time = engine.getTime(); + + if (coreParams.scriptFile.empty()) { + logger.info() << "nothing to do"; + return; + } + engine.setLevelConsumer([this](auto level) { + setLevel(std::move(level)); + }); + + logger.info() << "starting test " << coreParams.scriptFile; + auto process = scripting::start_coroutine(coreParams.scriptFile); + + double targetDelta = 1.0 / static_cast(TPS); + double delta = targetDelta; + auto begin = system_clock::now(); + auto startupTime = begin; + + while (process->isActive()) { + if (engine.isQuitSignal()) { + process->terminate(); + logger.info() << "script has been terminated due to quit signal"; + break; + } + if (coreParams.testMode) { + time.step(delta); + } else { + auto now = system_clock::now(); + time.update( + duration_cast(now - startupTime).count() / 1e6); + delta = time.getDelta(); + } + process->update(); + if (controller) { + controller->getLevel()->getWorld()->updateTimers(delta); + controller->update(glm::min(delta, 0.2), false); + } + + if (!coreParams.testMode) { + auto end = system_clock::now(); + platform::sleep(targetDelta * 1000 - + duration_cast(end - begin).count() / 1000); + begin = system_clock::now(); + } + } + logger.info() << "test finished"; +} + +void ServerMainloop::setLevel(std::unique_ptr level) { + if (level == nullptr) { + controller->onWorldQuit(); + engine.getPaths().setCurrentWorldFolder(fs::path()); + controller = nullptr; + } else { + controller = std::make_unique( + &engine, std::move(level), nullptr + ); + } +} diff --git a/src/TestMainloop.hpp b/src/ServerMainloop.hpp similarity index 73% rename from src/TestMainloop.hpp rename to src/ServerMainloop.hpp index 06ffae25a..28cfbd9de 100644 --- a/src/TestMainloop.hpp +++ b/src/ServerMainloop.hpp @@ -6,12 +6,12 @@ class Level; class LevelController; class Engine; -class TestMainloop { +class ServerMainloop { Engine& engine; std::unique_ptr controller; public: - TestMainloop(Engine& engine); - ~TestMainloop(); + ServerMainloop(Engine& engine); + ~ServerMainloop(); void run(); diff --git a/src/TestMainloop.cpp b/src/TestMainloop.cpp deleted file mode 100644 index 334870591..000000000 --- a/src/TestMainloop.cpp +++ /dev/null @@ -1,54 +0,0 @@ -#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/constants.hpp b/src/constants.hpp index c8de5a8dd..6f4a6ce6e 100644 --- a/src/constants.hpp +++ b/src/constants.hpp @@ -35,6 +35,11 @@ inline constexpr int CHUNK_D = 16; inline constexpr uint VOXEL_USER_BITS = 8; inline constexpr uint VOXEL_USER_BITS_OFFSET = sizeof(blockstate_t)*8-VOXEL_USER_BITS; +/// @brief % unordered map max average buckets load factor. +/// Low value gives significant performance impact by minimizing collisions and +/// lookup latency. Default value (1.0) shows x2 slower work. +inline constexpr float CHUNKS_MAP_MAX_LOAD_FACTOR = 0.1f; + /// @brief chunk volume (count of voxels per Chunk) inline constexpr int CHUNK_VOL = (CHUNK_W * CHUNK_H * CHUNK_D); diff --git a/src/content/ContentLoader.cpp b/src/content/ContentLoader.cpp index 1e954a557..fee559e4f 100644 --- a/src/content/ContentLoader.cpp +++ b/src/content/ContentLoader.cpp @@ -220,11 +220,11 @@ void ContentLoader::loadBlock( } // block model - std::string modelTypeName; + std::string modelTypeName = to_string(def.model); root.at("model").get(modelTypeName); root.at("model-name").get(def.modelName); if (auto model = BlockModel_from(modelTypeName)) { - if (*model == BlockModel::custom) { + if (*model == BlockModel::custom && def.customModelRaw == nullptr) { if (root.has("model-primitives")) { def.customModelRaw = root["model-primitives"]; } else if (def.modelName.empty()) { @@ -246,7 +246,7 @@ void ContentLoader::loadBlock( root.at("material").get(def.material); // rotation profile - std::string profile = "none"; + std::string profile = def.rotations.name; root.at("rotation").get(profile); def.rotatable = profile != "none"; @@ -285,8 +285,6 @@ void ContentLoader::loadBlock( ); aabb.b += aabb.a; def.hitboxes = {aabb}; - } else { - def.hitboxes = {AABB()}; } // block light emission [r, g, b] where r,g,b in range [0..15] diff --git a/src/debug/Logger.cpp b/src/debug/Logger.cpp index e8f4c95b7..970b2102d 100644 --- a/src/debug/Logger.cpp +++ b/src/debug/Logger.cpp @@ -23,10 +23,16 @@ Logger::Logger(std::string name) : name(std::move(name)) { void Logger::log( LogLevel level, const std::string& name, const std::string& message ) { + if (level == LogLevel::print) { + std::cout << "[" << name << "] " << message << std::endl; + return; + } + using namespace std::chrono; std::stringstream ss; switch (level) { + case LogLevel::print: case LogLevel::debug: #ifdef NDEBUG return; diff --git a/src/debug/Logger.hpp b/src/debug/Logger.hpp index 6b931fe0a..9505affd4 100644 --- a/src/debug/Logger.hpp +++ b/src/debug/Logger.hpp @@ -5,7 +5,7 @@ #include namespace debug { - enum class LogLevel { debug, info, warning, error }; + enum class LogLevel { print, debug, info, warning, error }; class Logger; @@ -60,5 +60,10 @@ namespace debug { LogMessage warning() { return LogMessage(this, LogLevel::warning); } + + /// @brief Print-debugging tool (printed without header) + LogMessage print() { + return LogMessage(this, LogLevel::print); + } }; } diff --git a/src/engine.cpp b/src/engine.cpp index 3c7d4bb5e..2f8466d6e 100644 --- a/src/engine.cpp +++ b/src/engine.cpp @@ -36,7 +36,7 @@ #include "window/Window.hpp" #include "world/Level.hpp" #include "Mainloop.hpp" -#include "TestMainloop.hpp" +#include "ServerMainloop.hpp" #include #include @@ -87,7 +87,7 @@ Engine::Engine(CoreParameters coreParameters) auto resdir = paths.getResourcesFolder(); - controller = std::make_unique(this); + controller = std::make_unique(*this); if (!params.headless) { if (Window::initialize(&settings.display)){ throw initialize_error("could not initialize window"); @@ -101,7 +101,7 @@ Engine::Engine(CoreParameters coreParameters) gui = std::make_unique(); if (ENGINE_DEBUG_BUILD) { - menus::create_version_label(this); + menus::create_version_label(*this); } } audio::initialize(settings.audio.enabled.get() && !params.headless); @@ -118,7 +118,7 @@ Engine::Engine(CoreParameters coreParameters) paths.getResourcesFolder() )); } - keepAlive(settings.ui.language.observe([=](auto lang) { + keepAlive(settings.ui.language.observe([this](auto lang) { setLanguage(lang); }, true)); @@ -173,7 +173,7 @@ void Engine::saveScreenshot() { void Engine::run() { if (params.headless) { - TestMainloop(*this).run(); + ServerMainloop(*this).run(); } else { Mainloop(*this).run(); } @@ -447,7 +447,7 @@ void Engine::setScreen(std::shared_ptr screen) { void Engine::setLanguage(std::string locale) { langs::setup(paths.getResourcesFolder(), std::move(locale), contentPacks); if (gui) { - gui->getMenu()->setPageLoader(menus::create_page_loader(this)); + gui->getMenu()->setPageLoader(menus::create_page_loader(*this)); } } @@ -461,6 +461,17 @@ void Engine::onWorldClosed() { levelConsumer(nullptr); } +void Engine::quit() { + quitSignal = true; + if (!isHeadless()) { + Window::setShouldClose(true); + } +} + +bool Engine::isQuitSignal() const { + return quitSignal; +} + gui::GUI* Engine::getGUI() { return gui.get(); } @@ -491,8 +502,8 @@ std::vector& Engine::getBasePacks() { return basePacks; } -EnginePaths* Engine::getPaths() { - return &paths; +EnginePaths& Engine::getPaths() { + return paths; } ResPaths* Engine::getResPaths() { diff --git a/src/engine.hpp b/src/engine.hpp index 63a780845..f10fb3a07 100644 --- a/src/engine.hpp +++ b/src/engine.hpp @@ -48,9 +48,10 @@ class initialize_error : public std::runtime_error { struct CoreParameters { bool headless = false; + bool testMode = false; std::filesystem::path resFolder {"res"}; std::filesystem::path userFolder {"."}; - std::filesystem::path testFile; + std::filesystem::path scriptFile; }; class Engine : public util::ObjectsKeeper { @@ -73,6 +74,7 @@ class Engine : public util::ObjectsKeeper { std::unique_ptr gui; Time time; consumer> levelConsumer; + bool quitSignal = false; void loadControls(); void loadSettings(); @@ -129,7 +131,7 @@ class Engine : public util::ObjectsKeeper { EngineSettings& getSettings(); /// @brief Get engine filesystem paths source - EnginePaths* getPaths(); + EnginePaths& getPaths(); /// @brief Get engine resource paths controller ResPaths* getResPaths(); @@ -137,6 +139,10 @@ class Engine : public util::ObjectsKeeper { void onWorldOpen(std::unique_ptr level); void onWorldClosed(); + void quit(); + + bool isQuitSignal() const; + /// @brief Get current Content instance const Content* getContent() const; diff --git a/src/files/engine_paths.cpp b/src/files/engine_paths.cpp index 268573313..0ecea7b85 100644 --- a/src/files/engine_paths.cpp +++ b/src/files/engine_paths.cpp @@ -132,7 +132,7 @@ std::filesystem::path EnginePaths::getSettingsFile() const { return userFilesFolder / SETTINGS_FILE; } -std::vector EnginePaths::scanForWorlds() { +std::vector EnginePaths::scanForWorlds() const { std::vector folders; auto folder = getWorldsFolder(); @@ -189,7 +189,7 @@ std::tuple EnginePaths::parsePath(std::string_view pat std::filesystem::path EnginePaths::resolve( const std::string& path, bool throwErr -) { +) const { auto [prefix, filename] = EnginePaths::parsePath(path); if (prefix.empty()) { throw files_access_error("no entry point specified"); diff --git a/src/files/engine_paths.hpp b/src/files/engine_paths.hpp index c52892437..61be1757b 100644 --- a/src/files/engine_paths.hpp +++ b/src/files/engine_paths.hpp @@ -39,9 +39,9 @@ class EnginePaths { void setContentPacks(std::vector* contentPacks); - std::vector scanForWorlds(); + std::vector scanForWorlds() const; - std::filesystem::path resolve(const std::string& path, bool throwErr = true); + std::filesystem::path resolve(const std::string& path, bool throwErr = true) const; static std::tuple parsePath(std::string_view view); diff --git a/src/frontend/debug_panel.cpp b/src/frontend/debug_panel.cpp index 3470cdb14..a56078034 100644 --- a/src/frontend/debug_panel.cpp +++ b/src/frontend/debug_panel.cpp @@ -42,7 +42,7 @@ static std::shared_ptr