Merge branch 'headless-mode' into Fix-usage-vcpkg-for-windows-and-add-cmake-preset-for-vscode

This commit is contained in:
Stepanov Igor
2024-12-25 14:58:24 +03:00
56 changed files with 454 additions and 107 deletions
+526
View File
@@ -0,0 +1,526 @@
#include "Engine.hpp"
#ifndef GLEW_STATIC
#define GLEW_STATIC
#endif
#include "debug/Logger.hpp"
#include "assets/AssetsLoader.hpp"
#include "audio/audio.hpp"
#include "coders/GLSLExtension.hpp"
#include "coders/imageio.hpp"
#include "coders/json.hpp"
#include "coders/toml.hpp"
#include "coders/commons.hpp"
#include "content/Content.hpp"
#include "content/ContentBuilder.hpp"
#include "content/ContentLoader.hpp"
#include "core_defs.hpp"
#include "files/files.hpp"
#include "frontend/locale.hpp"
#include "frontend/menu.hpp"
#include "frontend/screens/Screen.hpp"
#include "graphics/render/ModelsGenerator.hpp"
#include "graphics/core/DrawContext.hpp"
#include "graphics/core/ImageData.hpp"
#include "graphics/core/Shader.hpp"
#include "graphics/ui/GUI.hpp"
#include "objects/rigging.hpp"
#include "logic/EngineController.hpp"
#include "logic/CommandsInterpreter.hpp"
#include "logic/scripting/scripting.hpp"
#include "network/Network.hpp"
#include "util/listutil.hpp"
#include "util/platform.hpp"
#include "window/Camera.hpp"
#include "window/Events.hpp"
#include "window/input.hpp"
#include "window/Window.hpp"
#include "world/Level.hpp"
#include "Mainloop.hpp"
#include "ServerMainloop.hpp"
#include <iostream>
#include <assert.h>
#include <glm/glm.hpp>
#include <unordered_set>
#include <functional>
#include <utility>
static debug::Logger logger("engine");
namespace fs = std::filesystem;
static void create_channel(Engine* engine, std::string name, NumberSetting& setting) {
if (name != "master") {
audio::create_channel(name);
}
engine->keepAlive(setting.observe([=](auto value) {
audio::get_channel(name)->setVolume(value*value);
}, true));
}
static std::unique_ptr<ImageData> load_icon(const fs::path& resdir) {
try {
auto file = resdir / fs::u8path("textures/misc/icon.png");
if (fs::exists(file)) {
return imageio::read(file);
}
} catch (const std::exception& err) {
logger.error() << "could not load window icon: " << err.what();
}
return nullptr;
}
Engine::Engine(CoreParameters coreParameters)
: params(std::move(coreParameters)),
settings(),
settingsHandler({settings}),
interpreter(std::make_unique<cmd::CommandsInterpreter>()),
network(network::Network::create(settings.network)) {
logger.info() << "engine version: " << ENGINE_VERSION_STRING;
if (params.headless) {
logger.info() << "headless mode is enabled";
}
paths.setResourcesFolder(params.resFolder);
paths.setUserFilesFolder(params.userFolder);
paths.prepare();
if (!params.scriptFile.empty()) {
paths.setScriptFolder(params.scriptFile.parent_path());
}
loadSettings();
auto resdir = paths.getResourcesFolder();
controller = std::make_unique<EngineController>(*this);
if (!params.headless) {
if (Window::initialize(&settings.display)){
throw initialize_error("could not initialize window");
}
time.set(Window::time());
if (auto icon = load_icon(resdir)) {
icon->flipY();
Window::setIcon(icon.get());
}
loadControls();
gui = std::make_unique<gui::GUI>();
if (ENGINE_DEBUG_BUILD) {
menus::create_version_label(*this);
}
}
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);
bool langNotSet = settings.ui.language.get() == "auto";
if (langNotSet) {
settings.ui.language.set(langs::locale_by_envlocale(
platform::detect_locale(),
paths.getResourcesFolder()
));
}
keepAlive(settings.ui.language.observe([this](auto lang) {
setLanguage(lang);
}, true));
scripting::initialize(this);
basePacks = files::read_list(resdir/fs::path("config/builtins.list"));
}
void Engine::loadSettings() {
fs::path settings_file = paths.getSettingsFile();
if (fs::is_regular_file(settings_file)) {
logger.info() << "loading settings";
std::string text = files::read_string(settings_file);
try {
toml::parse(settingsHandler, settings_file.string(), text);
} catch (const parsing_error& err) {
logger.error() << err.errorLog();
throw;
}
}
}
void Engine::loadControls() {
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);
Events::loadBindings(controls_file.u8string(), text, BindType::BIND);
}
}
void Engine::onAssetsLoaded() {
assets->setup();
gui->onAssetsLoad(assets.get());
}
void Engine::updateHotkeys() {
if (Events::jpressed(keycode::F2)) {
saveScreenshot();
}
if (Events::jpressed(keycode::F11)) {
settings.display.fullscreen.toggle();
}
}
void Engine::saveScreenshot() {
auto image = Window::takeScreenshot();
image->flipY();
fs::path filename = paths.getNewScreenshotFile("png");
imageio::write(filename.string(), image.get());
logger.info() << "saved screenshot as " << filename.u8string();
}
void Engine::run() {
if (params.headless) {
ServerMainloop(*this).run();
} else {
Mainloop(*this).run();
}
}
void Engine::postUpdate() {
network->update();
postRunnables.run();
}
void Engine::updateFrontend() {
double delta = time.getDelta();
updateHotkeys();
audio::update(delta);
gui->act(delta, Viewport(Window::width, Window::height));
screen->update(delta);
}
void Engine::nextFrame() {
Window::setFramerate(
Window::isIconified() && settings.display.limitFpsIconified.get()
? 20
: settings.display.framerate.get()
);
Window::swapBuffers();
Events::pollEvents();
}
void Engine::renderFrame() {
screen->draw(time.getDelta());
Viewport viewport(Window::width, Window::height);
DrawContext ctx(nullptr, viewport, nullptr);
gui->draw(ctx, *assets);
}
void Engine::saveSettings() {
logger.info() << "saving settings";
files::write_string(paths.getSettingsFile(), toml::stringify(settingsHandler));
if (!params.headless) {
logger.info() << "saving bindings";
files::write_string(paths.getControlsFile(), Events::writeBindings());
}
}
Engine::~Engine() {
saveSettings();
logger.info() << "shutting down";
if (screen) {
screen->onEngineShutdown();
screen.reset();
}
content.reset();
assets.reset();
interpreter.reset();
if (gui) {
gui.reset();
logger.info() << "gui finished";
}
audio::close();
network.reset();
clearKeepedObjects();
scripting::close();
logger.info() << "scripting finished";
if (!params.headless) {
Window::terminate();
logger.info() << "window closed";
}
logger.info() << "engine finished";
}
EngineController* Engine::getController() {
return controller.get();
}
cmd::CommandsInterpreter* Engine::getCommandsInterpreter() {
return interpreter.get();
}
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")
});
return manager;
}
void Engine::setLevelConsumer(consumer<std::unique_ptr<Level>> levelConsumer) {
this->levelConsumer = std::move(levelConsumer);
}
void Engine::loadAssets() {
logger.info() << "loading assets";
Shader::preprocessor->setPaths(resPaths.get());
auto new_assets = std::make_unique<Assets>();
AssetsLoader loader(new_assets.get(), resPaths.get());
AssetsLoader::addDefaults(loader, content.get());
// no need
// correct log messages order is more useful
bool threading = false; // look at two upper lines
if (threading) {
auto task = loader.startTask([=](){});
task->waitForEnd();
} else {
while (loader.hasNext()) {
loader.loadNext();
}
}
assets = std::move(new_assets);
if (content == nullptr) {
return;
}
for (auto& [name, def] : content->blocks.getDefs()) {
if (def->model == BlockModel::custom && def->modelName.empty()) {
assets->store(
std::make_unique<model::Model>(
ModelsGenerator::loadCustomBlockModel(
def->customModelRaw, *assets, !def->shadeless
)
),
name + ".model"
);
def->modelName = def->name + ".model";
}
}
for (auto& [name, def] : content->items.getDefs()) {
assets->store(
std::make_unique<model::Model>(
ModelsGenerator::generate(*def, *content, *assets)
),
name + ".model"
);
}
}
static void load_configs(const fs::path& root) {
auto configFolder = root/fs::path("config");
auto bindsFile = configFolder/fs::path("bindings.toml");
if (fs::is_regular_file(bindsFile)) {
Events::loadBindings(
bindsFile.u8string(), files::read_string(bindsFile), BindType::BIND
);
}
}
void Engine::loadContent() {
scripting::cleanup();
auto resdir = paths.getResourcesFolder();
std::vector<std::string> names;
for (auto& pack : contentPacks) {
names.push_back(pack.id);
}
ContentBuilder contentBuilder;
corecontent::setup(paths, contentBuilder);
paths.setContentPacks(&contentPacks);
PacksManager manager = createPacksManager(paths.getCurrentWorldFolder());
manager.scan();
names = manager.assembly(names);
contentPacks = manager.getAll(names);
auto corePack = ContentPack::createCore(paths);
// Setup filesystem entry points
std::vector<PathsRoot> resRoots {
{"core", corePack.folder}
};
for (auto& pack : contentPacks) {
resRoots.push_back({pack.id, pack.folder});
}
resPaths = std::make_unique<ResPaths>(resdir, resRoots);
// Load content
{
ContentLoader(&corePack, contentBuilder, *resPaths).load();
load_configs(corePack.folder);
}
for (auto& pack : contentPacks) {
ContentLoader(&pack, contentBuilder, *resPaths).load();
load_configs(pack.folder);
}
content = contentBuilder.build();
interpreter->reset();
scripting::on_content_load(content.get());
ContentLoader::loadScripts(*content);
langs::setup(resdir, langs::current->getId(), contentPacks);
if (!isHeadless()) {
loadAssets();
onAssetsLoaded();
}
}
void Engine::resetContent() {
scripting::cleanup();
auto resdir = paths.getResourcesFolder();
std::vector<PathsRoot> resRoots;
{
auto pack = ContentPack::createCore(paths);
resRoots.push_back({"core", pack.folder});
load_configs(pack.folder);
}
auto manager = createPacksManager(fs::path());
manager.scan();
for (const auto& pack : manager.getAll(basePacks)) {
resRoots.push_back({pack.id, pack.folder});
}
resPaths = std::make_unique<ResPaths>(resdir, resRoots);
contentPacks.clear();
content.reset();
langs::setup(resdir, langs::current->getId(), contentPacks);
loadAssets();
onAssetsLoaded();
contentPacks = manager.getAll(basePacks);
}
void Engine::loadWorldContent(const fs::path& folder) {
contentPacks.clear();
auto packNames = ContentPack::worldPacksList(folder);
PacksManager manager;
manager.setSources({
folder/fs::path("content"),
paths.getUserFilesFolder()/fs::path("content"),
paths.getResourcesFolder()/fs::path("content")
});
manager.scan();
contentPacks = manager.getAll(manager.assembly(packNames));
paths.setCurrentWorldFolder(folder);
loadContent();
}
void Engine::loadAllPacks() {
PacksManager manager = createPacksManager(paths.getCurrentWorldFolder());
manager.scan();
auto allnames = manager.getAllNames();
contentPacks = manager.getAll(manager.assembly(allnames));
}
void Engine::setScreen(std::shared_ptr<Screen> screen) {
// reset audio channels (stop all sources)
audio::reset_channel(audio::get_channel_index("regular"));
audio::reset_channel(audio::get_channel_index("ambient"));
this->screen = std::move(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));
}
}
void Engine::onWorldOpen(std::unique_ptr<Level> level) {
logger.info() << "world open";
levelConsumer(std::move(level));
}
void Engine::onWorldClosed() {
logger.info() << "world closed";
levelConsumer(nullptr);
}
void Engine::quit() {
quitSignal = true;
if (!isHeadless()) {
Window::setShouldClose(true);
}
}
bool Engine::isQuitSignal() const {
return quitSignal;
}
gui::GUI* Engine::getGUI() {
return gui.get();
}
EngineSettings& Engine::getSettings() {
return settings;
}
Assets* Engine::getAssets() {
return assets.get();
}
const Content* Engine::getContent() const {
return content.get();
}
std::vector<ContentPack> Engine::getAllContentPacks() {
auto packs = getContentPacks();
packs.insert(packs.begin(), ContentPack::createCore(paths));
return packs;
}
std::vector<ContentPack>& Engine::getContentPacks() {
return contentPacks;
}
std::vector<std::string>& Engine::getBasePacks() {
return basePacks;
}
EnginePaths& Engine::getPaths() {
return paths;
}
ResPaths* Engine::getResPaths() {
return resPaths.get();
}
std::shared_ptr<Screen> Engine::getScreen() {
return screen;
}
SettingsHandler& Engine::getSettingsHandler() {
return settingsHandler;
}
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;
}
+179
View File
@@ -0,0 +1,179 @@
#pragma once
#include "delegates.hpp"
#include "typedefs.hpp"
#include "settings.hpp"
#include "assets/Assets.hpp"
#include "content/content_fwd.hpp"
#include "content/ContentPack.hpp"
#include "content/PacksManager.hpp"
#include "files/engine_paths.hpp"
#include "files/settings_io.hpp"
#include "util/ObjectsKeeper.hpp"
#include "PostRunnables.hpp"
#include "Time.hpp"
#include <filesystem>
#include <memory>
#include <stdexcept>
#include <string>
#include <vector>
class Level;
class Screen;
class EnginePaths;
class ResPaths;
class EngineController;
class SettingsHandler;
struct EngineSettings;
namespace gui {
class GUI;
}
namespace cmd {
class CommandsInterpreter;
}
namespace network {
class Network;
}
class initialize_error : public std::runtime_error {
public:
initialize_error(const std::string& message) : std::runtime_error(message) {}
};
struct CoreParameters {
bool headless = false;
bool testMode = false;
std::filesystem::path resFolder {"res"};
std::filesystem::path userFolder {"."};
std::filesystem::path scriptFile;
};
class Engine : public util::ObjectsKeeper {
CoreParameters params;
EngineSettings settings;
SettingsHandler settingsHandler;
EnginePaths paths;
std::unique_ptr<Assets> assets;
std::shared_ptr<Screen> screen;
std::vector<ContentPack> contentPacks;
std::unique_ptr<Content> content;
std::unique_ptr<ResPaths> resPaths;
std::unique_ptr<EngineController> controller;
std::unique_ptr<cmd::CommandsInterpreter> interpreter;
std::unique_ptr<network::Network> network;
std::vector<std::string> basePacks;
std::unique_ptr<gui::GUI> gui;
PostRunnables postRunnables;
Time time;
consumer<std::unique_ptr<Level>> levelConsumer;
bool quitSignal = false;
void loadControls();
void loadSettings();
void saveSettings();
void updateHotkeys();
void loadAssets();
public:
Engine(CoreParameters coreParameters);
~Engine();
/// @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();
/// @brief Set screen (scene).
/// nullptr may be used to delete previous screen before creating new one,
/// not-null value must be set before next frame
/// @param screen nullable screen
void setScreen(std::shared_ptr<Screen> screen);
/// @brief Change locale to specified
/// @param locale isolanguage_ISOCOUNTRY (example: en_US)
void setLanguage(std::string locale);
/// @brief Load all selected content-packs and reload assets
void loadContent();
void resetContent();
/// @brief Collect world content-packs and load content
/// @see loadContent
/// @param folder world folder
void loadWorldContent(const fs::path& folder);
/// @brief Collect all available content-packs from res/content
void loadAllPacks();
/// @brief Get active assets storage instance
Assets* getAssets();
/// @brief Get main UI controller
gui::GUI* getGUI();
/// @brief Get writeable engine settings structure instance
EngineSettings& getSettings();
/// @brief Get engine filesystem paths source
EnginePaths& getPaths();
/// @brief Get engine resource paths controller
ResPaths* getResPaths();
void onWorldOpen(std::unique_ptr<Level> level);
void onWorldClosed();
void quit();
bool isQuitSignal() const;
/// @brief Get current Content instance
const Content* getContent() const;
/// @brief Get selected content packs
std::vector<ContentPack>& getContentPacks();
std::vector<ContentPack> getAllContentPacks();
std::vector<std::string>& getBasePacks();
/// @brief Get current screen
std::shared_ptr<Screen> getScreen();
/// @brief Enqueue function call to the end of current frame in draw thread
void postRunnable(const runnable& callback) {
postRunnables.postRunnable(callback);
}
void saveScreenshot();
EngineController* getController();
cmd::CommandsInterpreter* getCommandsInterpreter();
PacksManager createPacksManager(const fs::path& worldFolder);
void setLevelConsumer(consumer<std::unique_ptr<Level>> levelConsumer);
SettingsHandler& getSettingsHandler();
network::Network& getNetwork();
Time& getTime();
const CoreParameters& getCoreParameters() const;
bool isHeadless() const;
};
+43
View File
@@ -0,0 +1,43 @@
#include "Mainloop.hpp"
#include "Engine.hpp"
#include "debug/Logger.hpp"
#include "frontend/screens/MenuScreen.hpp"
#include "frontend/screens/LevelScreen.hpp"
#include "window/Window.hpp"
#include "world/Level.hpp"
static debug::Logger logger("mainloop");
Mainloop::Mainloop(Engine& engine) : engine(engine) {
}
void Mainloop::run() {
auto& time = engine.getTime();
engine.setLevelConsumer([this](auto level) {
if (level == nullptr) {
// destroy LevelScreen and run quit callbacks
engine.setScreen(nullptr);
// create and go to menu screen
engine.setScreen(std::make_shared<MenuScreen>(engine));
} else {
engine.setScreen(std::make_shared<LevelScreen>(engine, std::move(level)));
}
});
logger.info() << "starting menu screen";
engine.setScreen(std::make_shared<MenuScreen>(engine));
logger.info() << "main loop started";
while (!Window::isShouldClose()){
time.update(Window::time());
engine.updateFrontend();
if (!Window::isIconified()) {
engine.renderFrame();
}
engine.postUpdate();
engine.nextFrame();
}
logger.info() << "main loop stopped";
}
+11
View File
@@ -0,0 +1,11 @@
#pragma once
class Engine;
class Mainloop {
Engine& engine;
public:
Mainloop(Engine& engine);
void run();
};
+29
View File
@@ -0,0 +1,29 @@
#pragma once
#include <queue>
#include <mutex>
#include "delegates.hpp"
class PostRunnables {
std::queue<runnable> runnables;
std::recursive_mutex mutex;
public:
void postRunnable(runnable task) {
std::lock_guard<std::recursive_mutex> lock(mutex);
runnables.push(std::move(task));
}
void run() {
std::queue<runnable> tasksToRun;
{
std::lock_guard<std::recursive_mutex> lock(mutex);
std::swap(tasksToRun, runnables);
}
while (!tasksToRun.empty()) {
auto& task = tasksToRun.front();
task();
tasksToRun.pop();
}
}
};
+85
View File
@@ -0,0 +1,85 @@
#include "ServerMainloop.hpp"
#include "Engine.hpp"
#include "logic/scripting/scripting.hpp"
#include "logic/LevelController.hpp"
#include "interfaces/Process.hpp"
#include "debug/Logger.hpp"
#include "world/Level.hpp"
#include "world/World.hpp"
#include "util/platform.hpp"
#include <chrono>
using namespace std::chrono;
static debug::Logger logger("mainloop");
inline constexpr int TPS = 20;
ServerMainloop::ServerMainloop(Engine& engine) : engine(engine) {
}
ServerMainloop::~ServerMainloop() = default;
void ServerMainloop::run() {
const auto& coreParams = engine.getCoreParameters();
auto& time = engine.getTime();
if (coreParams.scriptFile.empty()) {
logger.info() << "nothing to do";
return;
}
engine.setLevelConsumer([this](auto level) {
setLevel(std::move(level));
});
logger.info() << "starting test " << coreParams.scriptFile;
auto process = scripting::start_coroutine(coreParams.scriptFile);
double targetDelta = 1.0 / static_cast<double>(TPS);
double delta = targetDelta;
auto begin = system_clock::now();
auto startupTime = begin;
while (process->isActive()) {
if (engine.isQuitSignal()) {
process->terminate();
logger.info() << "script has been terminated due to quit signal";
break;
}
if (coreParams.testMode) {
time.step(delta);
} else {
auto now = system_clock::now();
time.update(
duration_cast<microseconds>(now - startupTime).count() / 1e6);
delta = time.getDelta();
}
process->update();
if (controller) {
controller->getLevel()->getWorld()->updateTimers(delta);
controller->update(glm::min(delta, 0.2), false);
}
if (!coreParams.testMode) {
auto end = system_clock::now();
platform::sleep(targetDelta * 1000 -
duration_cast<microseconds>(end - begin).count() / 1000);
begin = system_clock::now();
}
}
logger.info() << "test finished";
}
void ServerMainloop::setLevel(std::unique_ptr<Level> level) {
if (level == nullptr) {
controller->onWorldQuit();
engine.getPaths().setCurrentWorldFolder(fs::path());
controller = nullptr;
} else {
controller = std::make_unique<LevelController>(
&engine, std::move(level), nullptr
);
}
}
+19
View File
@@ -0,0 +1,19 @@
#pragma once
#include <memory>
class Level;
class LevelController;
class Engine;
class ServerMainloop {
Engine& engine;
std::unique_ptr<LevelController> controller;
public:
ServerMainloop(Engine& engine);
~ServerMainloop();
void run();
void setLevel(std::unique_ptr<Level> level);
};
+35
View File
@@ -0,0 +1,35 @@
#pragma once
#include <stdint.h>
class Time {
uint64_t frame = 0;
double lastTime = 0.0;
double delta = 0.0;
public:
Time() {}
void update(double currentTime) {
frame++;
delta = currentTime - lastTime;
lastTime = currentTime;
}
void step(double delta) {
frame++;
lastTime += delta;
this->delta = delta;
}
void set(double currentTime) {
lastTime = currentTime;
}
double getDelta() const {
return delta;
}
double getTime() const {
return lastTime;
}
};