Merge branch 'main' into continue-sdl-support-implementation

This commit is contained in:
MihailRis 2025-11-21 19:04:30 +03:00
commit 7f7533f33e
214 changed files with 5299 additions and 1731 deletions

View file

@ -112,7 +112,7 @@ target_compile_options(
-Wno-sign-compare
-Wno-unknown-pragmas
>
$<$<CXX_COMPILER_ID:Clang>:
$<$<CXX_COMPILER_ID:GNU>:
-Wduplicated-branches
-Wduplicated-cond
>)

View file

@ -9,7 +9,7 @@
#include "content/Content.hpp"
#include "content/ContentPack.hpp"
#include "debug/Logger.hpp"
#include "io/engine_paths.hpp"
#include "engine/EnginePaths.hpp"
#include "io/io.hpp"
#include "graphics/core/Texture.hpp"
#include "logic/scripting/scripting.hpp"

View file

@ -15,7 +15,7 @@
#include "coders/vec3.hpp"
#include "constants.hpp"
#include "debug/Logger.hpp"
#include "io/engine_paths.hpp"
#include "engine/EnginePaths.hpp"
#include "io/io.hpp"
#include "frontend/UiDocument.hpp"
#include "graphics/core/Atlas.hpp"
@ -184,7 +184,7 @@ assetload::postfunc assetload::atlas(
if (!append_atlas(builder, file)) continue;
}
std::set<std::string> names = builder.getNames();
Atlas* atlas = builder.build(2, false).release();
Atlas* atlas = builder.build(ATLAS_EXTRUSION, false).release();
return [=](auto assets) {
atlas->prepare();
assets->store(std::unique_ptr<Atlas>(atlas), name);
@ -501,7 +501,7 @@ static bool load_animation(
}
if (!append_atlas(builder, file)) continue;
}
auto srcAtlas = builder.build(2, true);
auto srcAtlas = builder.build(ATLAS_EXTRUSION, true);
if (frameList.empty()) {
for (const auto& frameName : builder.getNames()) {
frameList.emplace_back(frameName, 0);

View file

@ -5,11 +5,41 @@
#include "debug/Logger.hpp"
#include "alutil.hpp"
#include "../MemoryPCMStream.hpp"
static debug::Logger logger("al-audio");
using namespace audio;
const char* alc_error_to_string(ALCenum error) {
switch (error) {
case ALC_NO_ERROR:
return "no error";
case ALC_INVALID_DEVICE:
return "invalid device handle";
case ALC_INVALID_CONTEXT:
return "invalid context handle";
case ALC_INVALID_ENUM:
return "invalid enum parameter passed to an ALC call";
case ALC_INVALID_VALUE:
return "invalid value parameter passed to an ALC call";
case ALC_OUT_OF_MEMORY:
return "out of memory";
default:
return "unknown ALC error";
}
}
static bool check_alc_errors(ALCdevice* device, const char* context) {
ALCenum error = alcGetError(device);
if (error == ALC_NO_ERROR) {
return false;
}
logger.error() << context << ": " << alc_error_to_string(error) << "("
<< error << ")";
return true;
}
ALSound::ALSound(
ALAudio* al, uint buffer, const std::shared_ptr<PCM>& pcm, bool keepPCM
)
@ -37,6 +67,70 @@ std::unique_ptr<Speaker> ALSound::newInstance(int priority, int channel) const {
return speaker;
}
ALInputDevice::ALInputDevice(
ALAudio* al,
ALCdevice* device,
uint channels,
uint bitsPerSample,
uint sampleRate
)
: al(al),
device(device),
channels(channels),
bitsPerSample(bitsPerSample),
sampleRate(sampleRate) {
const ALCchar* deviceName = alcGetString(device, ALC_CAPTURE_DEVICE_SPECIFIER);
if (deviceName) {
deviceSpecifier = std::string(deviceName);
} else {
logger.warning() << "could not retrieve input device specifier";
}
}
ALInputDevice::~ALInputDevice() {
alcCaptureCloseDevice(device);
check_alc_errors(device, "alcCaptureCloseDevice");
}
void ALInputDevice::startCapture() {
alcCaptureStart(device);
check_alc_errors(device, "alcCaptureStart");
}
void ALInputDevice::stopCapture() {
alcCaptureStop(device);
check_alc_errors(device, "alcCaptureStop");
}
uint ALInputDevice::getChannels() const {
return channels;
}
uint ALInputDevice::getSampleRate() const {
return sampleRate;
}
uint ALInputDevice::getBitsPerSample() const {
return bitsPerSample;
}
const std::string& ALInputDevice::getDeviceSpecifier() const {
return deviceSpecifier;
}
size_t ALInputDevice::read(char* buffer, size_t bufferSize) {
ALCint samplesCount = 0;
alcGetIntegerv(device, ALC_CAPTURE_SAMPLES, sizeof(samplesCount), &samplesCount);
check_alc_errors(device, "alcGetIntegerv(ALC_CAPTURE_SAMPLES)");
size_t samplesRead = std::min<ALCsizei>(
samplesCount, bufferSize / channels / (bitsPerSample >> 3)
);
alcCaptureSamples(device, buffer, samplesRead);
check_alc_errors(device, "alcCaptureSamples");
return samplesRead * channels * (bitsPerSample >> 3);
}
ALStream::ALStream(
ALAudio* al, std::shared_ptr<PCMStream> source, bool keepSource
)
@ -81,9 +175,10 @@ std::unique_ptr<Speaker> ALStream::createSpeaker(bool loop, int channel) {
for (uint i = 0; i < ALStream::STREAM_BUFFERS; i++) {
uint free_buffer = al->getFreeBuffer();
if (!preloadBuffer(free_buffer, loop)) {
break;
unusedBuffers.push(free_buffer);
} else {
AL_CHECK(alSourceQueueBuffers(free_source, 1, &free_buffer));
}
AL_CHECK(alSourceQueueBuffers(free_source, 1, &free_buffer));
}
return std::make_unique<ALSpeaker>(al, free_source, PRIORITY_HIGH, channel);
}
@ -130,11 +225,11 @@ void ALStream::unqueueBuffers(uint alsource) {
uint ALStream::enqueueBuffers(uint alsource) {
uint preloaded = 0;
if (!unusedBuffers.empty()) {
uint first_buffer = unusedBuffers.front();
if (preloadBuffer(first_buffer, loop)) {
uint firstBuffer = unusedBuffers.front();
if (preloadBuffer(firstBuffer, loop)) {
preloaded++;
unusedBuffers.pop();
AL_CHECK(alSourceQueueBuffers(alsource, 1, &first_buffer));
AL_CHECK(alSourceQueueBuffers(alsource, 1, &firstBuffer));
}
}
return preloaded;
@ -144,14 +239,14 @@ void ALStream::update(double delta) {
if (this->speaker == 0) {
return;
}
auto p_speaker = audio::get_speaker(this->speaker);
if (p_speaker == nullptr) {
auto speaker = audio::get_speaker(this->speaker);
if (speaker == nullptr) {
this->speaker = 0;
return;
}
ALSpeaker* alspeaker = dynamic_cast<ALSpeaker*>(p_speaker);
ALSpeaker* alspeaker = dynamic_cast<ALSpeaker*>(speaker);
assert(alspeaker != nullptr);
if (alspeaker->stopped) {
if (alspeaker->manuallyStopped) {
this->speaker = 0;
return;
}
@ -162,11 +257,11 @@ void ALStream::update(double delta) {
uint preloaded = enqueueBuffers(alsource);
// alspeaker->stopped is assigned to false at ALSpeaker::play(...)
if (p_speaker->isStopped() && !alspeaker->stopped) { //TODO: -V560 false-positive?
if (speaker->isStopped() && !alspeaker->manuallyStopped) { //TODO: -V560 false-positive?
if (preloaded) {
p_speaker->play();
} else {
p_speaker->stop();
speaker->play();
} else if (isStopOnEnd()){
speaker->stop();
}
}
}
@ -207,6 +302,14 @@ void ALStream::setTime(duration_t time) {
}
}
bool ALStream::isStopOnEnd() const {
return stopOnEnd;
}
void ALStream::setStopOnEnd(bool flag) {
stopOnEnd = flag;
}
ALSpeaker::ALSpeaker(ALAudio* al, uint source, int priority, int channel)
: al(al), priority(priority), channel(channel), source(source) {
}
@ -273,7 +376,7 @@ void ALSpeaker::setLoop(bool loop) {
void ALSpeaker::play() {
paused = false;
stopped = false;
manuallyStopped = false;
auto p_channel = get_channel(this->channel);
AL_CHECK(alSourcef(
source,
@ -289,7 +392,7 @@ void ALSpeaker::pause() {
}
void ALSpeaker::stop() {
stopped = true;
manuallyStopped = true;
if (source) {
AL_CHECK(alSourceStop(source));
@ -353,6 +456,13 @@ int ALSpeaker::getPriority() const {
return priority;
}
bool ALSpeaker::isManuallyStopped() const {
return manuallyStopped;
}
static bool alc_enumeration_ext = false;
ALAudio::ALAudio(ALCdevice* device, ALCcontext* context)
: device(device), context(context) {
ALCint size;
@ -365,9 +475,15 @@ ALAudio::ALAudio(ALCdevice* device, ALCcontext* context)
maxSources = attrs[i + 1];
}
}
auto devices = getAvailableDevices();
logger.info() << "devices:";
for (auto& name : devices) {
auto outputDevices = getOutputDeviceNames();
logger.info() << "output devices:";
for (auto& name : outputDevices) {
logger.info() << " " << name;
}
auto inputDevices = getInputDeviceNames();
logger.info() << "input devices:";
for (auto& name : inputDevices) {
logger.info() << " " << name;
}
}
@ -385,8 +501,10 @@ ALAudio::~ALAudio() {
AL_CHECK(alDeleteBuffers(1, &buffer));
}
AL_CHECK(alcMakeContextCurrent(context));
alcMakeContextCurrent(nullptr);
check_alc_errors(device, "alcMakeContextCurrent");
alcDestroyContext(context);
check_alc_errors(device, "alcDestroyContext");
if (!alcCloseDevice(device)) {
logger.error() << "device not closed!";
}
@ -411,7 +529,71 @@ std::unique_ptr<Stream> ALAudio::openStream(
return std::make_unique<ALStream>(this, stream, keepSource);
}
std::vector<std::string> ALAudio::getInputDeviceNames() {
std::vector<std::string> devices;
if (!alc_enumeration_ext) {
logger.warning() << "enumeration extension is not available";
return devices;
}
auto deviceList = alcGetString(nullptr, ALC_CAPTURE_DEVICE_SPECIFIER);
if (deviceList == nullptr) {
logger.warning() << "no input devices found";
return devices;
}
while (*deviceList) {
std::string deviceName(deviceList);
devices.push_back(deviceName);
deviceList += deviceName.length() + 1;
}
return devices;
}
std::vector<std::string> ALAudio::getOutputDeviceNames() {
std::vector<std::string> devices;
if (!alc_enumeration_ext) {
logger.warning() << "enumeration extension is not available";
return devices;
}
auto deviceList = alcGetString(nullptr, ALC_ALL_DEVICES_SPECIFIER);
if (deviceList == nullptr) {
logger.warning() << "no input devices found";
return devices;
}
while (*deviceList) {
std::string deviceName(deviceList);
devices.push_back(deviceName);
deviceList += deviceName.length() + 1;
}
return devices;
}
std::unique_ptr<InputDevice> ALAudio::openInputDevice(
const std::string& deviceName, uint sampleRate, uint channels, uint bitsPerSample
) {
uint bps = bitsPerSample >> 3;
ALCdevice* device = alcCaptureOpenDevice(
deviceName.empty() ? nullptr : deviceName.c_str(),
sampleRate,
AL::to_al_format(channels, bitsPerSample),
sampleRate * channels * bps / 8
);
if (check_alc_errors(device, "alcCaptureOpenDevice"))
return nullptr;
return std::make_unique<ALInputDevice>(
this, device, channels, bitsPerSample, sampleRate
);
}
std::unique_ptr<ALAudio> ALAudio::create() {
alc_enumeration_ext = alcIsExtensionPresent(nullptr, "ALC_ENUMERATION_EXT");
ALCdevice* device = alcOpenDevice(nullptr);
if (device == nullptr) return nullptr;
ALCcontext* context = alcCreateContext(device, nullptr);
@ -468,24 +650,6 @@ void ALAudio::freeBuffer(uint buffer) {
freebuffers.push_back(buffer);
}
std::vector<std::string> ALAudio::getAvailableDevices() const {
std::vector<std::string> devicesVec;
const ALCchar* devices;
devices = alcGetString(device, ALC_DEVICE_SPECIFIER);
if (!AL_GET_ERROR()) {
return devicesVec;
}
const char* ptr = devices;
do {
devicesVec.emplace_back(ptr);
ptr += devicesVec.back().size() + 1;
} while (ptr[0]);
return devicesVec;
}
void ALAudio::setListener(
glm::vec3 position, glm::vec3 velocity, glm::vec3 at, glm::vec3 up
) {

View file

@ -58,6 +58,7 @@ namespace audio {
bool keepSource;
char buffer[BUFFER_SIZE];
bool loop = false;
bool stopOnEnd = false;
bool preloadBuffer(uint buffer, bool loop);
void unqueueBuffers(uint alsource);
@ -80,6 +81,39 @@ namespace audio {
void setTime(duration_t time) override;
static inline constexpr uint STREAM_BUFFERS = 3;
bool isStopOnEnd() const override;
void setStopOnEnd(bool stopOnEnd) override;
};
class ALInputDevice : public InputDevice {
public:
ALInputDevice(
ALAudio* al,
ALCdevice* device,
uint channels,
uint bitsPerSample,
uint sampleRate
);
~ALInputDevice() override;
void startCapture() override;
void stopCapture() override;
uint getChannels() const override;
uint getSampleRate() const override;
uint getBitsPerSample() const override;
const std::string& getDeviceSpecifier() const override;
size_t read(char* buffer, size_t bufferSize) override;
private:
ALAudio* al;
ALCdevice* device;
uint channels;
uint bitsPerSample;
uint sampleRate;
std::string deviceSpecifier;
};
/// @brief AL source adapter
@ -90,7 +124,7 @@ namespace audio {
float volume = 0.0f;
public:
ALStream* stream = nullptr;
bool stopped = true;
bool manuallyStopped = true;
bool paused = false;
uint source;
duration_t duration = 0.0f;
@ -130,6 +164,8 @@ namespace audio {
bool isRelative() const override;
int getPriority() const override;
bool isManuallyStopped() const override;
};
class ALAudio : public Backend {
@ -152,15 +188,24 @@ namespace audio {
void freeSource(uint source);
void freeBuffer(uint buffer);
std::vector<std::string> getAvailableDevices() const;
std::unique_ptr<Sound> createSound(
std::shared_ptr<PCM> pcm, bool keepPCM
) override;
std::unique_ptr<Stream> openStream(
std::shared_ptr<PCMStream> stream, bool keepSource
) override;
std::unique_ptr<InputDevice> openInputDevice(
const std::string& deviceName,
uint sampleRate,
uint channels,
uint bitsPerSample
) override;
std::vector<std::string> getOutputDeviceNames() override;
std::vector<std::string> getInputDeviceNames() override;
void setListener(
glm::vec3 position,
glm::vec3 velocity,

View file

@ -0,0 +1,67 @@
#include "MemoryPCMStream.hpp"
#include <cstring>
using namespace audio;
MemoryPCMStream::MemoryPCMStream(
uint sampleRate, uint channels, uint bitsPerSample
)
: sampleRate(sampleRate), channels(channels), bitsPerSample(bitsPerSample) {
}
void MemoryPCMStream::feed(util::span<ubyte> bytes) {
buffer.insert(buffer.end(), bytes.begin(), bytes.end());
}
bool MemoryPCMStream::isOpen() const {
return open;
}
void MemoryPCMStream::close() {
open = false;
buffer = {};
}
size_t MemoryPCMStream::read(char* dst, size_t bufferSize) {
if (!open) {
return PCMStream::ERROR;
}
if (buffer.empty()) {
return 0;
}
size_t count = std::min<size_t>(bufferSize, buffer.size());
std::memcpy(dst, buffer.data(), count);
buffer.erase(buffer.begin(), buffer.begin() + count);
return count;
}
size_t MemoryPCMStream::getTotalSamples() const {
return 0;
}
duration_t MemoryPCMStream::getTotalDuration() const {
return 0.0;
}
uint MemoryPCMStream::getChannels() const {
return channels;
}
uint MemoryPCMStream::getSampleRate() const {
return sampleRate;
}
uint MemoryPCMStream::getBitsPerSample() const {
return bitsPerSample;
}
bool MemoryPCMStream::isSeekable() const {
return false;
}
void MemoryPCMStream::seek(size_t position) {}
size_t MemoryPCMStream::available() const {
return buffer.size();
}

View file

@ -0,0 +1,44 @@
#pragma once
#include <vector>
#include "audio.hpp"
#include "util/span.hpp"
namespace audio {
class MemoryPCMStream : public PCMStream {
public:
MemoryPCMStream(uint sampleRate, uint channels, uint bitsPerSample);
void feed(util::span<ubyte> bytes);
bool isOpen() const override;
void close() override;
size_t read(char* buffer, size_t bufferSize) override;
size_t getTotalSamples() const override;
duration_t getTotalDuration() const override;
uint getChannels() const override;
uint getSampleRate() const override;
uint getBitsPerSample() const override;
bool isSeekable() const override;
void seek(size_t position) override;
size_t available() const;
private:
uint sampleRate;
uint channels;
uint bitsPerSample;
bool open = true;
std::vector<ubyte> buffer;
};
}

View file

@ -61,6 +61,13 @@ namespace audio {
void setTime(duration_t time) override {
}
bool isStopOnEnd() const override {
return false;
}
void setStopOnEnd(bool stopOnEnd) override {
}
};
class NoAudio : public Backend {
@ -71,10 +78,24 @@ namespace audio {
std::unique_ptr<Sound> createSound(
std::shared_ptr<PCM> pcm, bool keepPCM
) override;
std::unique_ptr<Stream> openStream(
std::shared_ptr<PCMStream> stream, bool keepSource
) override;
std::unique_ptr<InputDevice> openInputDevice(
const std::string& deviceName, uint sampleRate, uint channels, uint bitsPerSample
) override {
return nullptr;
}
std::vector<std::string> getInputDeviceNames() override {
return {};
}
std::vector<std::string> getOutputDeviceNames() override {
return {};
}
void setListener(
glm::vec3 position, glm::vec3 velocity, glm::vec3 at, glm::vec3 up
) override {

View file

@ -151,6 +151,8 @@ public:
}
};
static std::unique_ptr<InputDevice> input_device = nullptr;
void audio::initialize(bool enabled, AudioSettings& settings) {
enabled = enabled && settings.enabled.get();
if (enabled) {
@ -180,6 +182,15 @@ void audio::initialize(bool enabled, AudioSettings& settings) {
audio::get_channel(channel.name)->setVolume(value * value);
}, true));
}
input_device = backend->openInputDevice("", 44100, 1, 16);
if (input_device) {
input_device->startCapture();
}
}
InputDevice* audio::get_input_device() {
return input_device.get();
}
std::unique_ptr<PCM> audio::load_PCM(const io::path& file, bool headerOnly) {
@ -242,6 +253,38 @@ std::unique_ptr<Stream> audio::open_stream(
return backend->openStream(std::move(stream), keepSource);
}
std::unique_ptr<InputDevice> audio::open_input_device(
const std::string& deviceName, uint sampleRate, uint channels, uint bitsPerSample
) {
return backend->openInputDevice(
deviceName, sampleRate, channels, bitsPerSample
);
}
std::vector<std::string> audio::get_input_devices_names() {
return backend->getInputDeviceNames();
}
std::vector<std::string> audio::get_output_devices_names() {
return backend->getOutputDeviceNames();
}
void audio::set_input_device(const std::string& deviceName) {
auto newDevice = backend->openInputDevice(deviceName, 44100, 1, 16);
if (newDevice == nullptr) {
logger.error() << "could not open input device: " << deviceName;
return;
}
if (input_device) {
input_device->stopCapture();
}
input_device = std::move(newDevice);
if (input_device) {
input_device->startCapture();
}
}
void audio::set_listener(
glm::vec3 position, glm::vec3 velocity, glm::vec3 lookAt, glm::vec3 up
) {
@ -421,8 +464,15 @@ void audio::update(double delta) {
speaker->update(channel);
}
if (speaker->isStopped()) {
streams.erase(it->first);
it = speakers.erase(it);
auto foundStream = streams.find(it->first);
if (foundStream == streams.end() ||
(!speaker->isManuallyStopped() &&
foundStream->second->isStopOnEnd())) {
streams.erase(it->first);
it = speakers.erase(it);
} else {
it++;
}
} else {
it++;
}
@ -458,6 +508,9 @@ void audio::reset_channel(int index) {
}
void audio::close() {
if (input_device) {
input_device->stopCapture();
}
speakers.clear();
delete backend;
backend = nullptr;

View file

@ -24,6 +24,8 @@ namespace audio {
/// @brief streams and important sounds
constexpr inline int PRIORITY_HIGH = 10;
constexpr inline size_t MAX_INPUT_SAMPLES = 22050;
class Speaker;
/// @brief Audio speaker states
@ -108,6 +110,31 @@ namespace audio {
}
};
class InputDevice {
public:
virtual ~InputDevice() {};
virtual void startCapture() = 0;
virtual void stopCapture() = 0;
/// @brief Get number of audio channels
/// @return 1 if mono, 2 if stereo
virtual uint getChannels() const = 0;
/// @brief Get audio sampling frequency
/// @return number of mono samples per second
virtual uint getSampleRate() const = 0;
/// @brief Get number of bits per mono sample
/// @return 8 or 16
virtual uint getBitsPerSample() const = 0;
/// @brief Read available data to buffer.
/// @return size of data received or PCMStream::ERROR in case of error
virtual size_t read(char* buffer, size_t bufferSize) = 0;
/// @brief Get device specifier string
virtual const std::string& getDeviceSpecifier() const = 0;
};
/// @brief audio::PCMStream is a data source for audio::Stream
class PCMStream {
public:
@ -121,6 +148,10 @@ namespace audio {
/// (always equals bufferSize if seekable and looped)
virtual size_t readFully(char* buffer, size_t bufferSize, bool loop);
/// @brief Read available data to buffer
/// @param buffer destination buffer
/// @param bufferSize destination buffer size
/// @return count of received bytes or PCMStream::ERROR
virtual size_t read(char* buffer, size_t bufferSize) = 0;
/// @brief Close stream
@ -195,6 +226,9 @@ namespace audio {
/// @brief Set playhead to the selected time
/// @param time selected time
virtual void setTime(duration_t time) = 0;
virtual bool isStopOnEnd() const = 0;
virtual void setStopOnEnd(bool stopOnEnd) = 0;
};
/// @brief Sound is an audio asset that supposed to support many
@ -329,6 +363,8 @@ namespace audio {
inline bool isStopped() const {
return getState() == State::stopped;
}
virtual bool isManuallyStopped() const = 0;
};
class Backend {
@ -341,12 +377,20 @@ namespace audio {
virtual std::unique_ptr<Stream> openStream(
std::shared_ptr<PCMStream> stream, bool keepSource
) = 0;
virtual std::unique_ptr<InputDevice> openInputDevice(
const std::string& deviceName,
uint sampleRate,
uint channels,
uint bitsPerSample
) = 0;
virtual void setListener(
glm::vec3 position,
glm::vec3 velocity,
glm::vec3 lookAt,
glm::vec3 up
) = 0;
virtual std::vector<std::string> getInputDeviceNames() = 0;
virtual std::vector<std::string> getOutputDeviceNames() = 0;
virtual void update(double delta) = 0;
/// @brief Check if backend is an abstraction that does not internally
@ -402,6 +446,28 @@ namespace audio {
std::shared_ptr<PCMStream> stream, bool keepSource
);
/// @brief Open audio input device
/// @param sampleRate sample rate
/// @param channels channels count (1 - mono, 2 - stereo)
/// @param bitsPerSample number of bits per sample (8 or 16)
/// @return new InputDevice instance or nullptr
std::unique_ptr<InputDevice> open_input_device(
const std::string& deviceName,
uint sampleRate,
uint channels,
uint bitsPerSample
);
/// @brief Retrieve names of available audio input devices
/// @return list of device names
std::vector<std::string> get_input_devices_names();
/// @brief Retrieve names of available audio output devices
/// @return list of device names
std::vector<std::string> get_output_devices_names();
void set_input_device(const std::string& deviceName);
/// @brief Configure 3D listener
/// @param position listener position
/// @param velocity listener velocity (used for Doppler effect)
@ -515,6 +581,8 @@ namespace audio {
/// @brief Stop all playing audio in channel, reset channel state
void reset_channel(int channel);
InputDevice* get_input_device();
/// @brief Finalize audio system
void close();
};

View file

@ -6,7 +6,7 @@
#include <utility>
#include "debug/Logger.hpp"
#include "io/engine_paths.hpp"
#include "engine/EnginePaths.hpp"
#include "typedefs.hpp"
#include "util/stringutil.hpp"
#include "coders/json.hpp"

View file

@ -1,3 +1,4 @@
#define VC_ENABLE_REFLECTION
#include "imageio.hpp"
#include <functional>
@ -7,28 +8,34 @@
#include "io/io.hpp"
#include "png.hpp"
using namespace imageio;
using image_reader =
std::function<std::unique_ptr<ImageData>(const ubyte*, size_t)>;
using image_writer = std::function<void(const std::string&, const ImageData*)>;
static std::unordered_map<std::string, image_reader> readers {
{".png", png::load_image},
static std::unordered_map<ImageFileFormat, image_reader> readers {
{ImageFileFormat::PNG, png::load_image},
};
static std::unordered_map<std::string, image_writer> writers {
{".png", png::write_image},
static std::unordered_map<ImageFileFormat, image_writer> writers {
{ImageFileFormat::PNG, png::write_image},
};
bool imageio::is_read_supported(const std::string& extension) {
return readers.find(extension) != readers.end();
return extension == ".png";
}
bool imageio::is_write_supported(const std::string& extension) {
return writers.find(extension) != writers.end();
return extension == ".png";
}
std::unique_ptr<ImageData> imageio::read(const io::path& file) {
auto found = readers.find(file.extension());
ImageFileFormat format;
if (!ImageFileFormatMeta.getItem(file.extension().substr(1), format)) {
throw std::runtime_error("unsupported image format");
}
auto found = readers.find(format);
if (found == readers.end()) {
throw std::runtime_error(
"file format is not supported (read): " + file.string()
@ -44,8 +51,25 @@ std::unique_ptr<ImageData> imageio::read(const io::path& file) {
}
}
std::unique_ptr<ImageData> imageio::decode(
ImageFileFormat format, util::span<ubyte> src
) {
auto found = readers.find(format);
try {
return std::unique_ptr<ImageData>(found->second(src.data(), src.size()));
} catch (const std::runtime_error& err) {
throw std::runtime_error(
"could not to decode image: " + std::string(err.what())
);
}
}
void imageio::write(const io::path& file, const ImageData* image) {
auto found = writers.find(file.extension());
ImageFileFormat format;
if (!ImageFileFormatMeta.getItem(file.extension().substr(1), format)) {
throw std::runtime_error("unsupported image format");
}
auto found = writers.find(format);
if (found == writers.end()) {
throw std::runtime_error(
"file format is not supported (write): " + file.string()
@ -53,3 +77,14 @@ void imageio::write(const io::path& file, const ImageData* image) {
}
return found->second(io::resolve(file).u8string(), image);
}
util::Buffer<unsigned char> imageio::encode(
ImageFileFormat format, const ImageData& image
) {
switch (format) {
case ImageFileFormat::PNG:
return png::encode_image(image);
default:
throw std::runtime_error("file format is not supported for encoding");
}
}

View file

@ -4,10 +4,22 @@
#include <string>
#include "io/fwd.hpp"
#include "util/Buffer.hpp"
#include "util/EnumMetadata.hpp"
#include "util/span.hpp"
#include "typedefs.hpp"
class ImageData;
namespace imageio {
enum class ImageFileFormat {
PNG
};
VC_ENUM_METADATA(ImageFileFormat)
{"png", ImageFileFormat::PNG},
VC_ENUM_END
inline const std::string PNG = ".png";
bool is_read_supported(const std::string& extension);
@ -15,4 +27,6 @@ namespace imageio {
std::unique_ptr<ImageData> read(const io::path& file);
void write(const io::path& file, const ImageData* image);
std::unique_ptr<ImageData> decode(ImageFileFormat format, util::span<ubyte> src);
util::Buffer<unsigned char> encode(ImageFileFormat format, const ImageData& image);
}

View file

@ -11,6 +11,60 @@
static debug::Logger logger("png-coder");
static util::Buffer<ubyte> write_to_memory(uint width, uint height, const ubyte* data, bool alpha) {
uint pixsize = alpha ? 4 : 3;
std::vector<ubyte> buffer;
png_structp png_ptr = png_create_write_struct(
PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr
);
png_infop info_ptr = png_create_info_struct(png_ptr);
png_set_write_fn(
png_ptr,
&buffer,
[](png_structp pngPtr, png_bytep data, png_size_t length) {
auto& buf = *reinterpret_cast<std::vector<ubyte>*>(png_get_io_ptr(pngPtr));
buf.insert(
buf.end(),
reinterpret_cast<ubyte*>(data),
reinterpret_cast<ubyte*>(data) + length
);
},
nullptr
);
png_set_IHDR(
png_ptr,
info_ptr,
width,
height,
8,
alpha ? PNG_COLOR_TYPE_RGBA : PNG_COLOR_TYPE_RGB,
PNG_INTERLACE_NONE,
PNG_COMPRESSION_TYPE_BASE,
PNG_FILTER_TYPE_BASE
);
png_write_info(png_ptr, info_ptr);
auto row = std::make_unique<png_byte[]>(pixsize * width);
for (uint y = 0; y < height; y++) {
for (uint x = 0; x < width; x++) {
for (uint i = 0; i < pixsize; i++) {
row[x * pixsize + i] =
(png_byte)data[(y * width + x) * pixsize + i];
}
}
png_write_row(png_ptr, row.get());
}
png_write_end(png_ptr, nullptr);
png_free_data(png_ptr, info_ptr, PNG_FREE_ALL, -1);
png_destroy_write_struct(&png_ptr, &info_ptr);
return util::Buffer<ubyte>(buffer.data(), buffer.size());
}
// returns 0 if all-right, 1 otherwise
static int png_write(
const char* filename, uint width, uint height, const ubyte* data, bool alpha
@ -230,3 +284,13 @@ void png::write_image(const std::string& filename, const ImageData* image) {
image->getFormat() == ImageFormat::rgba8888
);
}
util::Buffer<ubyte> png::encode_image(const ImageData& image) {
auto format = image.getFormat();
return write_to_memory(
image.getWidth(),
image.getHeight(),
image.getData(),
format == ImageFormat::rgba8888
);
}

View file

@ -4,6 +4,7 @@
#include <string>
#include "typedefs.hpp"
#include "util/Buffer.hpp"
class Texture;
class ImageData;
@ -11,6 +12,7 @@ class ImageData;
namespace png {
std::unique_ptr<ImageData> load_image(const ubyte* bytes, size_t size);
void write_image(const std::string& filename, const ImageData* image);
util::Buffer<ubyte> encode_image(const ImageData& image);
std::unique_ptr<Texture> load_texture(const ubyte* bytes, size_t size);
std::unique_ptr<Texture> load_texture(const std::string& filename);
}

View file

@ -61,6 +61,8 @@ inline constexpr int ITEM_ICON_SIZE = 48;
inline constexpr int TRANSLUCENT_BLOCKS_SORT_INTERVAL = 8;
inline constexpr int ATLAS_EXTRUSION = 2;
inline const std::string SHADERS_FOLDER = "shaders";
inline const std::string TEXTURES_FOLDER = "textures";
inline const std::string FONTS_FOLDER = "fonts";

View file

@ -1,7 +1,7 @@
#include "ContentControl.hpp"
#include "io/io.hpp"
#include "io/engine_paths.hpp"
#include "engine/EnginePaths.hpp"
#include "Content.hpp"
#include "ContentPack.hpp"
#include "ContentBuilder.hpp"
@ -16,6 +16,13 @@ static void load_configs(Input& input, const io::path& root) {
auto configFolder = root / "config";
}
static std::vector<io::path> default_content_sources {
"world:content",
"user:content",
"project:content",
"res:content",
};
ContentControl::ContentControl(
const Project& project,
EnginePaths& paths,
@ -27,11 +34,7 @@ ContentControl::ContentControl(
postContent(std::move(postContent)),
basePacks(project.basePacks),
manager(std::make_unique<PacksManager>()) {
manager->setSources({
"world:content",
"user:content",
"res:content",
});
manager->setSources(default_content_sources);
}
ContentControl::~ContentControl() = default;
@ -48,10 +51,10 @@ std::vector<std::string>& ContentControl::getBasePacks() {
return basePacks;
}
void ContentControl::resetContent() {
void ContentControl::resetContent(const std::vector<std::string>& nonReset) {
paths.setCurrentWorldFolder("");
scripting::cleanup();
scripting::cleanup(nonReset);
std::vector<PathsRoot> resRoots;
{
auto pack = ContentPack::createCore();
@ -67,6 +70,7 @@ void ContentControl::resetContent() {
scripting::on_content_reset();
setContentPacksRaw(manager->getAll(basePacks));
resetContentSources();
postContent();
}
@ -78,8 +82,6 @@ void ContentControl::loadContent(const std::vector<std::string>& names) {
}
void ContentControl::loadContent() {
scripting::cleanup();
std::vector<std::string> names;
for (auto& pack : contentPacks) {
names.push_back(pack.id);
@ -140,3 +142,15 @@ PacksManager& ContentControl::scan() {
manager->scan();
return *manager;
}
void ContentControl::setContentSources(std::vector<io::path> sources) {
manager->setSources(std::move(sources));
}
void ContentControl::resetContentSources() {
manager->setSources(default_content_sources);
}
const std::vector<io::path>& ContentControl::getContentSources() const {
return manager->getSources();
}

View file

@ -5,6 +5,7 @@
#include <string>
#include <functional>
#include "io/path.hpp"
#include "ContentPack.hpp"
class Content;
@ -13,10 +14,6 @@ class EnginePaths;
class Input;
struct Project;
namespace io {
class path;
}
class ContentControl {
public:
ContentControl(
@ -34,7 +31,7 @@ public:
std::vector<std::string>& getBasePacks();
/// @brief Reset content to base packs list
void resetContent();
void resetContent(const std::vector<std::string>& nonReset);
void loadContent(const std::vector<std::string>& names);
@ -46,6 +43,10 @@ public:
const std::vector<ContentPack>& getAllContentPacks() const;
PacksManager& scan();
void setContentSources(std::vector<io::path> sources);
void resetContentSources();
const std::vector<io::path>& getContentSources() const;
private:
EnginePaths& paths;
Input& input;

View file

@ -13,7 +13,7 @@
#include "objects/rigging.hpp"
#include "util/listutil.hpp"
#include "util/stringutil.hpp"
#include "io/engine_paths.hpp"
#include "engine/EnginePaths.hpp"
static debug::Logger logger("content-loader");

View file

@ -8,7 +8,7 @@
#include "coders/json.hpp"
#include "constants.hpp"
#include "data/dv.hpp"
#include "io/engine_paths.hpp"
#include "engine/EnginePaths.hpp"
#include "io/io.hpp"
#include "coders/commons.hpp"
#include "debug/Logger.hpp"

View file

@ -9,6 +9,10 @@
PacksManager::PacksManager() = default;
const std::vector<io::path>& PacksManager::getSources() const {
return sources;
}
void PacksManager::setSources(std::vector<io::path> sources) {
this->sources = std::move(sources);
}

View file

@ -12,6 +12,9 @@ class PacksManager {
public:
PacksManager();
/// @brief Get current content packs sources
const std::vector<io::path>& getSources() const;
/// @brief Set content packs sources (search folders)
void setSources(std::vector<io::path> sources);

View file

@ -6,7 +6,7 @@
#include "../ContentPack.hpp"
#include "io/io.hpp"
#include "io/engine_paths.hpp"
#include "engine/EnginePaths.hpp"
#include "logic/scripting/scripting.hpp"
#include "util/stringutil.hpp"
#include "world/generator/GeneratorDef.hpp"

View file

@ -4,7 +4,7 @@
#include "content/Content.hpp"
#include "content/ContentBuilder.hpp"
#include "io/io.hpp"
#include "io/engine_paths.hpp"
#include "engine/EnginePaths.hpp"
#include "window/input.hpp"
#include "voxels/Block.hpp"
#include "coders/toml.hpp"

View file

@ -197,6 +197,7 @@ bool DebuggingServer::performCommand(
connectionEstablished = true;
logger.info() << "client connection established";
connection->sendResponse("success");
return true;
}
if (!connectionEstablished) {
return false;

View file

@ -1,7 +1,7 @@
#include "Editor.hpp"
#include "engine/Engine.hpp"
#include "io/engine_paths.hpp"
#include "engine/EnginePaths.hpp"
#include "coders/syntax_parser.hpp"
#include "SyntaxProcessor.hpp"

View file

@ -1,8 +1,13 @@
#include "Project.hpp"
#include "data/dv_util.hpp"
#include "debug/Logger.hpp"
#include "io/io.hpp"
#include "io/path.hpp"
#include "logic/scripting/scripting.hpp"
static debug::Logger logger("project");
Project::~Project() = default;
dv::value Project::serialize() const {
@ -18,3 +23,23 @@ void Project::deserialize(const dv::value& src) {
src.at("title").get(title);
dv::get(src, "base_packs", basePacks);
}
void Project::loadProjectClientScript() {
io::path scriptFile = "project:project_client.lua";
if (io::exists(scriptFile)) {
logger.info() << "starting project client script";
clientScript = scripting::load_client_project_script(scriptFile);
} else {
logger.warning() << "project client script does not exists";
}
}
void Project::loadProjectStartScript() {
io::path scriptFile = "project:start.lua";
if (io::exists(scriptFile)) {
logger.info() << "starting project start script";
setupCoroutine = scripting::start_app_script(scriptFile);
} else {
logger.warning() << "project start script does not exists";
}
}

View file

@ -4,6 +4,7 @@
#include <vector>
#include <memory>
#include "interfaces/Process.hpp"
#include "interfaces/Serializable.hpp"
namespace scripting {
@ -15,9 +16,13 @@ struct Project : Serializable {
std::string title;
std::vector<std::string> basePacks;
std::unique_ptr<scripting::IClientProjectScript> clientScript;
std::unique_ptr<Process> setupCoroutine;
~Project();
dv::value serialize() const override;
void deserialize(const dv::value& src) override;
void loadProjectClientScript();
void loadProjectStartScript();
};

View file

@ -0,0 +1,15 @@
#pragma once
#include <string>
#include <filesystem>
struct CoreParameters {
bool headless = false;
bool testMode = false;
std::filesystem::path resFolder = "res";
std::filesystem::path userFolder = ".";
std::filesystem::path scriptFile;
std::filesystem::path projectFolder;
std::string debugServerString;
int tps = 20;
};

View file

@ -8,8 +8,6 @@
#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 "devtools/Editor.hpp"
@ -18,27 +16,28 @@
#include "content/ContentControl.hpp"
#include "core_defs.hpp"
#include "io/io.hpp"
#include "io/settings_io.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 "graphics/ui/elements/Menu.hpp"
#include "logic/EngineController.hpp"
#include "logic/CommandsInterpreter.hpp"
#include "logic/scripting/scripting.hpp"
#include "logic/scripting/scripting_hud.hpp"
#include "network/Network.hpp"
#include "util/platform.hpp"
#include "window/Camera.hpp"
#include "window/input.hpp"
#include "window/Window.hpp"
#include "world/Level.hpp"
#include "Mainloop.hpp"
#include "ServerMainloop.hpp"
#include "WindowControl.hpp"
#include "EnginePaths.hpp"
#include <iostream>
#include <assert.h>
@ -49,29 +48,6 @@
static debug::Logger logger("engine");
static std::unique_ptr<ImageData> load_icon() {
try {
auto file = "res:textures/misc/icon.png";
if (io::exists(file)) {
return imageio::read(file);
}
} catch (const std::exception& err) {
logger.error() << "could not load window icon: " << err.what();
}
return nullptr;
}
static std::unique_ptr<scripting::IClientProjectScript> load_client_project_script() {
io::path scriptFile = "project:project_client.lua";
if (io::exists(scriptFile)) {
logger.info() << "starting project script";
return scripting::load_client_project_script(scriptFile);
} else {
logger.warning() << "project script does not exists";
}
return nullptr;
}
Engine::Engine() = default;
Engine::~Engine() = default;
@ -86,7 +62,7 @@ Engine& Engine::getInstance() {
void Engine::onContentLoad() {
editor->loadTools();
langs::setup(langs::get_current(), paths.resPaths.collectRoots());
langs::setup(langs::get_current(), paths->resPaths.collectRoots());
if (isHeadless()) {
return;
@ -94,6 +70,7 @@ void Engine::onContentLoad() {
for (auto& pack : content->getAllContentPacks()) {
auto configFolder = pack.folder / "config";
auto bindsFile = configFolder / "bindings.toml";
logger.info() << "loading bindings: " << bindsFile.string();
if (io::is_regular_file(bindsFile)) {
input->getBindings().read(
toml::parse(
@ -107,29 +84,9 @@ void Engine::onContentLoad() {
}
void Engine::initializeClient() {
std::string title = project->title;
if (title.empty()) {
title = "VoxelCore v" +
std::to_string(ENGINE_VERSION_MAJOR) + "." +
std::to_string(ENGINE_VERSION_MINOR);
}
if (ENGINE_DEBUG_BUILD) {
title += " [debug]";
}
if (debuggingServer) {
title = "[debugging] " + title;
}
auto [window, input] = Window::initialize(&settings.display, title);
if (!window || !input){
throw initialize_error("could not initialize window");
}
window->setFramerate(settings.display.framerate.get());
windowControl = std::make_unique<WindowControl>(*this);
auto [window, input] = windowControl->initialize();
time.set(window->time());
if (auto icon = load_icon()) {
icon->flipY();
window->setIcon(icon.get());
}
this->window = std::move(window);
this->input = std::move(input);
@ -154,6 +111,14 @@ void Engine::initializeClient() {
},
true
));
keepAlive(this->input->addKeyCallback(Keycode::ESCAPE, [this]() {
auto& menu = *gui->getMenu();
if (menu.hasOpenPage() && menu.back()) {
return true;
}
return false;
}));
}
void Engine::initialize(CoreParameters coreParameters) {
@ -162,15 +127,12 @@ void Engine::initialize(CoreParameters coreParameters) {
logger.info() << "engine version: " << ENGINE_VERSION_STRING;
if (params.headless) {
logger.info() << "headless mode is enabled";
logger.info() << "engine runs in headless mode";
}
if (params.projectFolder.empty()) {
params.projectFolder = params.resFolder;
}
paths.setResourcesFolder(params.resFolder);
paths.setUserFilesFolder(params.userFolder);
paths.setProjectFolder(params.projectFolder);
paths.prepare();
paths = std::make_unique<EnginePaths>(params);
loadProject();
editor = std::make_unique<devtools::Editor>(*this);
@ -188,10 +150,6 @@ void Engine::initialize(CoreParameters coreParameters) {
);
}
}
if (!params.scriptFile.empty()) {
paths.setScriptFolder(params.scriptFile.parent_path());
}
loadSettings();
controller = std::make_unique<EngineController>(*this);
@ -205,7 +163,7 @@ void Engine::initialize(CoreParameters coreParameters) {
langs::locale_by_envlocale(platform::detect_locale())
);
}
content = std::make_unique<ContentControl>(*project, paths, *input, [this]() {
content = std::make_unique<ContentControl>(*project, *paths, *input, [this]() {
onContentLoad();
});
scripting::initialize(this);
@ -214,10 +172,13 @@ void Engine::initialize(CoreParameters coreParameters) {
gui->setPageLoader(scripting::create_page_loader());
}
keepAlive(settings.ui.language.observe([this](auto lang) {
langs::setup(lang, paths.resPaths.collectRoots());
langs::setup(lang, paths->resPaths.collectRoots());
}, true));
project->clientScript = load_client_project_script();
project->loadProjectStartScript();
if (!params.headless) {
project->loadProjectClientScript();
}
}
void Engine::loadSettings() {
@ -247,29 +208,17 @@ void Engine::loadControls() {
void Engine::updateHotkeys() {
if (input->jpressed(Keycode::F2)) {
saveScreenshot();
windowControl->saveScreenshot();
}
if (input->pressed(Keycode::LEFT_CONTROL) && input->pressed(Keycode::F3) &&
input->jpressed(Keycode::U)) {
gui->toggleDebug();
}
if (input->jpressed(Keycode::F11)) {
if (settings.display.windowMode.get() != static_cast<int>(WindowMode::FULLSCREEN)) {
settings.display.windowMode.set(static_cast<int>(WindowMode::FULLSCREEN));
} else {
settings.display.windowMode.set(static_cast<int>(WindowMode::WINDOWED));
}
windowControl->toggleFullscreen();
}
}
void Engine::saveScreenshot() {
auto image = window->takeScreenshot();
image->flipY();
io::path filename = paths.getNewScreenshotFile("png");
imageio::write(filename.string(), image.get());
logger.info() << "saved screenshot as " << filename.string();
}
void Engine::run() {
if (params.headless) {
ServerMainloop(*this).run();
@ -292,6 +241,12 @@ void Engine::detachDebugger() {
debuggingServer.reset();
}
void Engine::applicationTick() {
if (project->setupCoroutine && project->setupCoroutine->isActive()) {
project->setupCoroutine->update();
}
}
void Engine::updateFrontend() {
double delta = time.getDelta();
updateHotkeys();
@ -301,15 +256,8 @@ void Engine::updateFrontend() {
gui->postAct();
}
void Engine::nextFrame() {
window->setFramerate(
window->isIconified() && settings.display.limitFpsIconified.get()
? 20
: settings.display.framerate.get()
);
window->swapBuffers();
// todo: process it on another thread
input->pollEvents();
void Engine::nextFrame(bool waitForRefresh) {
windowControl->nextFrame(waitForRefresh);
}
void Engine::startPauseLoop() {
@ -328,7 +276,7 @@ void Engine::startPauseLoop() {
if (isHeadless()) {
platform::sleep(1.0 / params.tps * 1000);
} else {
nextFrame();
nextFrame(false);
}
}
if (initialCursorLocked) {
@ -399,17 +347,18 @@ void Engine::setLevelConsumer(OnWorldOpen levelConsumer) {
void Engine::loadAssets() {
logger.info() << "loading assets";
Shader::preprocessor->setPaths(&paths.resPaths);
Shader::preprocessor->setPaths(&paths->resPaths);
auto content = this->content->get();
auto new_assets = std::make_unique<Assets>();
AssetsLoader loader(*this, *new_assets, paths.resPaths);
AssetsLoader loader(*this, *new_assets, paths->resPaths);
AssetsLoader::addDefaults(loader, content);
// no need
// correct log messages order is more useful
bool threading = false; // look at two upper lines
// todo: before setting to true, check if GLSLExtension thread safe
bool threading = false; // look at three upper lines
if (threading) {
auto task = loader.startTask([=](){});
task->waitForEnd();
@ -446,6 +395,7 @@ void Engine::setScreen(std::shared_ptr<Screen> screen) {
}
if (project->clientScript && this->screen) {
project->clientScript->onScreenChange(this->screen->getName(), true);
window->setShouldRefresh();
}
}
@ -479,11 +429,11 @@ Assets* Engine::getAssets() {
}
EnginePaths& Engine::getPaths() {
return paths;
return *paths;
}
ResPaths& Engine::getResPaths() {
return paths.resPaths;
return paths->resPaths;
}
std::shared_ptr<Screen> Engine::getScreen() {

View file

@ -1,25 +1,27 @@
#pragma once
#include "delegates.hpp"
#include "typedefs.hpp"
#include "settings.hpp"
#include "io/engine_paths.hpp"
#include "io/settings_io.hpp"
#include "util/ObjectsKeeper.hpp"
#include "CoreParameters.hpp"
#include "PostRunnables.hpp"
#include "Time.hpp"
#include "delegates.hpp"
#include "settings.hpp"
#include "typedefs.hpp"
#include "util/ObjectsKeeper.hpp"
#include <memory>
#include <string>
class Window;
class Assets;
class Level;
class Screen;
class ContentControl;
class EngineController;
class EnginePaths;
class Input;
class Level;
class ResPaths;
class Screen;
class SettingsHandler;
class Window;
class WindowControl;
struct Project;
namespace gui {
@ -44,24 +46,12 @@ 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;
std::filesystem::path projectFolder;
std::string debugServerString;
int tps = 20;
};
using OnWorldOpen = std::function<void(std::unique_ptr<Level>, int64_t)>;
class Engine : public util::ObjectsKeeper {
CoreParameters params;
EngineSettings settings;
EnginePaths paths;
std::unique_ptr<EnginePaths> paths;
std::unique_ptr<Project> project;
std::unique_ptr<SettingsHandler> settingsHandler;
std::unique_ptr<Assets> assets;
@ -75,6 +65,7 @@ class Engine : public util::ObjectsKeeper {
std::unique_ptr<gui::GUI> gui;
std::unique_ptr<devtools::Editor> editor;
std::unique_ptr<devtools::DebuggingServer> debuggingServer;
std::unique_ptr<WindowControl> windowControl;
PostRunnables postRunnables;
Time time;
OnWorldOpen levelConsumer;
@ -105,9 +96,10 @@ public:
void postUpdate();
void applicationTick();
void updateFrontend();
void renderFrame();
void nextFrame();
void nextFrame(bool waitForRefresh);
void startPauseLoop();
/// @brief Set screen (scene).
@ -143,8 +135,6 @@ public:
postRunnables.postRunnable(callback);
}
void saveScreenshot();
EngineController* getController();
void setLevelConsumer(OnWorldOpen levelConsumer);

View file

@ -1,44 +1,57 @@
#include "engine_paths.hpp"
#include "EnginePaths.hpp"
#include "debug/Logger.hpp"
#include "io/devices/StdfsDevice.hpp"
#include "io/devices/MemoryDevice.hpp"
#include "io/devices/ZipFileDevice.hpp"
#include "maths/util.hpp"
#include "typedefs.hpp"
#include "util/platform.hpp"
#include "util/random.hpp"
#include "util/stringutil.hpp"
#include "world/files/WorldFiles.hpp"
#include <algorithm>
#include <array>
#include <chrono>
#include <sstream>
#include <stack>
#include "typedefs.hpp"
#include "util/stringutil.hpp"
#include "util/platform.hpp"
#include <stdexcept>
#include <utility>
#include "io/devices/StdfsDevice.hpp"
#include "io/devices/ZipFileDevice.hpp"
#include "world/files/WorldFiles.hpp"
#include "debug/Logger.hpp"
#include <chrono>
#include "maths/util.hpp"
template<int n>
static std::string generate_random_base64() {
auto now = std::chrono::high_resolution_clock::now();
auto seed = now.time_since_epoch().count();
util::PseudoRandom random(seed); // fixme: replace with safe random
ubyte bytes[n];
random.rand(bytes, n);
return util::base64_urlsafe_encode(bytes, n);
}
namespace fs = std::filesystem;
static debug::Logger logger("engine-paths");
static std::random_device random_device;
static inline io::path SCREENSHOTS_FOLDER = "user:screenshots";
static inline io::path CONTENT_FOLDER = "user:content";
static inline io::path WORLDS_FOLDER = "user:worlds";
void EnginePaths::prepare() {
static debug::Logger logger("engine-paths");
template<int n>
static std::string generate_random_base64() {
auto randomEngine = util::seeded_random_engine(random_device);
static std::uniform_int_distribution<integer_t> dist(0, 0xFF);
ubyte bytes[n];
for (size_t i = 0; i < n; i++) {
bytes[i] = dist(randomEngine);
}
return util::base64_urlsafe_encode(bytes, n);
}
EnginePaths::EnginePaths(CoreParameters& params)
: resourcesFolder(params.resFolder),
userFilesFolder(params.userFolder),
projectFolder(params.projectFolder) {
if (!params.scriptFile.empty()) {
scriptFolder = params.scriptFile.parent_path();
io::set_device("script", std::make_shared<io::StdfsDevice>(*scriptFolder));
}
io::set_device("res", std::make_shared<io::StdfsDevice>(resourcesFolder, false));
io::set_device("user", std::make_shared<io::StdfsDevice>(userFilesFolder));
io::set_device("project", std::make_shared<io::StdfsDevice>(projectFolder));
if (!io::is_directory("res:")) {
throw std::runtime_error(
@ -59,15 +72,15 @@ void EnginePaths::prepare() {
io::create_subdevice("config", "user", "config");
}
const std::filesystem::path& EnginePaths::getUserFilesFolder() const {
return userFilesFolder;
}
const std::filesystem::path& EnginePaths::getResourcesFolder() const {
std::filesystem::path EnginePaths::getResourcesFolder() const {
return resourcesFolder;
}
io::path EnginePaths::getNewScreenshotFile(const std::string& ext) {
std::filesystem::path EnginePaths::getUserFilesFolder() const {
return userFilesFolder;
}
io::path EnginePaths::getNewScreenshotFile(const std::string& ext) const {
auto folder = SCREENSHOTS_FOLDER;
if (!io::is_directory(folder)) {
io::create_directories(folder);
@ -95,10 +108,6 @@ io::path EnginePaths::getWorldsFolder() const {
return WORLDS_FOLDER;
}
io::path EnginePaths::getCurrentWorldFolder() {
return currentWorldFolder;
}
io::path EnginePaths::getWorldFolderByName(const std::string& name) {
return getWorldsFolder() / name;
}
@ -132,24 +141,6 @@ std::vector<io::path> EnginePaths::scanForWorlds() const {
return folders;
}
void EnginePaths::setUserFilesFolder(std::filesystem::path folder) {
this->userFilesFolder = std::move(folder);
}
void EnginePaths::setResourcesFolder(std::filesystem::path folder) {
this->resourcesFolder = std::move(folder);
}
void EnginePaths::setScriptFolder(std::filesystem::path folder) {
io::set_device("script", std::make_shared<io::StdfsDevice>(folder));
this->scriptFolder = std::move(folder);
}
void EnginePaths::setProjectFolder(std::filesystem::path folder) {
io::set_device("project", std::make_shared<io::StdfsDevice>(folder));
this->projectFolder = std::move(folder);
}
void EnginePaths::setCurrentWorldFolder(io::path folder) {
if (folder.empty()) {
io::remove_device("world");
@ -186,6 +177,18 @@ void EnginePaths::unmount(const std::string& name) {
mounted.erase(found);
}
std::string EnginePaths::createMemoryDevice() {
auto device = std::make_unique<io::MemoryDevice>();
std::string name;
do {
name = std::string("W.") + generate_random_base64<6>();
} while (std::find(mounted.begin(), mounted.end(), name) != mounted.end());
io::set_device(name, std::move(device));
mounted.push_back(name);
return name;
}
std::string EnginePaths::createWriteableDevice(const std::string& name) {
const auto& found = writeables.find(name);
if (found != writeables.end()) {

View file

@ -1,15 +1,15 @@
#pragma once
#include "io/io.hpp"
#include "data/dv.hpp"
#include "CoreParameters.hpp"
#include <unordered_map>
#include <stdexcept>
#include <optional>
#include <string>
#include <vector>
#include <tuple>
#include "io.hpp"
#include "data/dv.hpp"
struct PathsRoot {
std::string name;
io::path path;
@ -46,29 +46,22 @@ class EnginePaths {
public:
ResPaths resPaths;
void prepare();
EnginePaths(CoreParameters& params);
void setUserFilesFolder(std::filesystem::path folder);
const std::filesystem::path& getUserFilesFolder() const;
void setResourcesFolder(std::filesystem::path folder);
const std::filesystem::path& getResourcesFolder() const;
void setScriptFolder(std::filesystem::path folder);
void setProjectFolder(std::filesystem::path folder);
std::filesystem::path getResourcesFolder() const;
std::filesystem::path getUserFilesFolder() const;
io::path getWorldFolderByName(const std::string& name);
io::path getWorldsFolder() const;
void setCurrentWorldFolder(io::path folder);
io::path getCurrentWorldFolder();
io::path getNewScreenshotFile(const std::string& ext);
io::path getNewScreenshotFile(const std::string& ext) const;
std::string mount(const io::path& file);
void unmount(const std::string& name);
std::string createWriteableDevice(const std::string& name);
std::string createMemoryDevice();
void setEntryPoints(std::vector<PathsRoot> entryPoints);
@ -80,9 +73,9 @@ public:
static inline io::path CONTROLS_FILE = "user:controls.toml";
static inline io::path SETTINGS_FILE = "user:settings.toml";
private:
std::filesystem::path userFilesFolder {"."};
std::filesystem::path resourcesFolder {"res"};
std::filesystem::path projectFolder = resourcesFolder;
std::filesystem::path resourcesFolder;
std::filesystem::path userFilesFolder;
std::filesystem::path projectFolder;
io::path currentWorldFolder;
std::optional<std::filesystem::path> scriptFolder;
std::vector<PathsRoot> entryPoints;

View file

@ -18,6 +18,7 @@ Mainloop::Mainloop(Engine& engine) : engine(engine) {
void Mainloop::run() {
auto& time = engine.getTime();
auto& window = engine.getWindow();
auto& settings = engine.getSettings();
engine.setLevelConsumer([this](auto level, int64_t localPlayer) {
if (level == nullptr) {
@ -38,13 +39,17 @@ void Mainloop::run() {
logger.info() << "main loop started";
while (!window.isShouldClose()){
time.update(window.time());
engine.applicationTick();
engine.updateFrontend();
if (!window.isIconified()) {
engine.renderFrame();
}
engine.postUpdate();
engine.nextFrame();
engine.nextFrame(
settings.display.adaptiveFpsInMenu.get() &&
dynamic_cast<const MenuScreen*>(engine.getScreen().get()) != nullptr
);
}
logger.info() << "main loop stopped";
}

View file

@ -1,6 +1,7 @@
#include "ServerMainloop.hpp"
#include "Engine.hpp"
#include "EnginePaths.hpp"
#include "logic/scripting/scripting.hpp"
#include "logic/LevelController.hpp"
#include "interfaces/Process.hpp"
@ -32,8 +33,7 @@ void ServerMainloop::run() {
setLevel(std::move(level));
});
logger.info() << "starting test " << coreParams.scriptFile.string();
auto process = scripting::start_coroutine(
auto process = scripting::start_app_script(
"script:" + coreParams.scriptFile.filename().u8string()
);
@ -61,6 +61,7 @@ void ServerMainloop::run() {
controller->getLevel()->getWorld()->updateTimers(delta);
controller->update(glm::min(delta, 0.2), false);
}
engine.applicationTick();
engine.postUpdate();
if (!coreParams.testMode) {

View file

@ -0,0 +1,94 @@
#include "WindowControl.hpp"
#include "Engine.hpp"
#include "engine/EnginePaths.hpp"
#include "devtools/Project.hpp"
#include "coders/imageio.hpp"
#include "window/Window.hpp"
#include "window/input.hpp"
#include "debug/Logger.hpp"
#include "graphics/core/ImageData.hpp"
#include "util/platform.hpp"
static debug::Logger logger("window-control");
namespace {
static std::unique_ptr<ImageData> load_icon() {
try {
auto file = "res:textures/misc/icon.png";
if (io::exists(file)) {
return imageio::read(file);
}
} catch (const std::exception& err) {
logger.error() << "could not load window icon: " << err.what();
}
return nullptr;
}
}
WindowControl::WindowControl(Engine& engine) : engine(engine) {}
WindowControl::Result WindowControl::initialize() {
const auto& project = engine.getProject();
auto& settings = engine.getSettings();
std::string title = project.title;
if (!title.empty()) {
title += " - ";
}
title += "VoxelCore v" +
std::to_string(ENGINE_VERSION_MAJOR) + "." +
std::to_string(ENGINE_VERSION_MINOR);
if (ENGINE_DEBUG_BUILD) {
title += " [debug]";
}
if (engine.getDebuggingServer()) {
title = "[debugging] " + title;
}
auto [window, input] = Window::initialize(&settings.display, title);
if (!window || !input){
throw initialize_error("could not initialize window");
}
window->setFramerate(settings.display.framerate.get());
if (auto icon = load_icon()) {
icon->flipY();
window->setIcon(icon.get());
}
return Result {std::move(window), std::move(input)};
}
void WindowControl::saveScreenshot() {
auto& window = engine.getWindow();
const auto& paths = engine.getPaths();
auto image = window.takeScreenshot();
image->flipY();
io::path filename = paths.getNewScreenshotFile("png");
imageio::write(filename.string(), image.get());
logger.info() << "saved screenshot as " << filename.string();
}
void WindowControl::toggleFullscreen() {
auto& settings = engine.getSettings();
auto& windowMode = settings.display.windowMode;
if (windowMode.get() != static_cast<int>(WindowMode::FULLSCREEN)) {
windowMode.set(static_cast<int>(WindowMode::FULLSCREEN));
} else {
windowMode.set(static_cast<int>(WindowMode::WINDOWED));
}
}
void WindowControl::nextFrame(bool waitForRefresh) {
const auto& settings = engine.getSettings();
auto& window = engine.getWindow();
auto& input = engine.getInput();
window.setFramerate(
window.isIconified() && settings.display.limitFpsIconified.get()
? 20
: settings.display.framerate.get()
);
window.swapBuffers();
input.pollEvents(waitForRefresh && !window.checkShouldRefresh());
}

View file

@ -0,0 +1,26 @@
#pragma once
#include <memory>
class Window;
class Input;
class Engine;
class WindowControl {
public:
struct Result {
std::unique_ptr<Window> window;
std::unique_ptr<Input> input;
};
WindowControl(Engine& engine);
Result initialize();
void nextFrame(bool waitForRefresh);
void saveScreenshot();
void toggleFullscreen();
private:
Engine& engine;
};

View file

@ -62,7 +62,7 @@ void ContentGfxCache::refreshVariant(
}
}
}
models[def.rt.id] = std::move(model);
models[modelKey(def.rt.id, variantIndex)] = std::move(model);
}
}
@ -94,8 +94,8 @@ void ContentGfxCache::refresh() {
ContentGfxCache::~ContentGfxCache() = default;
const model::Model& ContentGfxCache::getModel(blockid_t id) const {
const auto& found = models.find(id);
const model::Model& ContentGfxCache::getModel(blockid_t id, uint8_t variant) const {
const auto& found = models.find(modelKey(id, variant));
if (found == models.end()) {
throw std::runtime_error("model not found");
}

View file

@ -27,7 +27,11 @@ class ContentGfxCache {
// array of block sides uv regions (6 per block)
std::unique_ptr<UVRegion[]> sideregions;
std::unordered_map<blockid_t, model::Model> models;
std::unordered_map<uint64_t, model::Model> models;
static inline uint64_t modelKey(blockid_t id, uint8_t variant) {
return (uint64_t(id) << 8) | uint64_t(variant & 0xFF);
}
void refreshVariant(
const Block& def,
@ -53,7 +57,7 @@ public:
return sideregions[getRegionIndex(id, variant, side, !dense)];
}
const model::Model& getModel(blockid_t id) const;
const model::Model& getModel(blockid_t id, uint8_t variant) const;
void refresh(const Block& block, const Atlas& atlas);

View file

@ -49,8 +49,7 @@ LevelFrontend::LevelFrontend(
auto sound = rassets.get<audio::Sound>(material->stepsSound);
glm::vec3 pos {};
auto soundsCamera = currentPlayer->currentCamera.get();
if (soundsCamera == currentPlayer->spCamera.get() ||
soundsCamera == currentPlayer->tpCamera.get()) {
if (currentPlayer->isCurrentCameraBuiltin()) {
soundsCamera = currentPlayer->fpCamera.get();
}
bool relative = player == currentPlayer &&

View file

@ -9,6 +9,7 @@
#include "graphics/ui/elements/TextBox.hpp"
#include "graphics/ui/elements/TrackBar.hpp"
#include "graphics/ui/elements/InputBindBox.hpp"
#include "graphics/ui/GUI.hpp"
#include "graphics/render/WorldRenderer.hpp"
#include "graphics/render/ParticlesRenderer.hpp"
#include "graphics/render/ChunksRenderer.hpp"
@ -43,10 +44,15 @@ static std::shared_ptr<Label> create_label(GUI& gui, wstringsupplier supplier) {
return label;
}
static bool should_keep_previous(GUI& gui) {
return !gui.getInput().isCursorLocked();
}
// TODO: move to xml
// TODO: move to xml finally
// TODO: move to xml finally
// TODO: move to xml finally
// TODO: move to xml finally
std::shared_ptr<UINode> create_debug_panel(
Engine& engine,
Level& level,
@ -135,7 +141,16 @@ std::shared_ptr<UINode> create_debug_panel(
std::to_wstring(player.getId());
}));
panel->add(create_label(gui, [&]() -> std::wstring {
const auto& vox = player.selection.vox;
// TODO: move to xml finally
static voxel prevVox = {BLOCK_VOID, {}};
auto vox = player.selection.vox;
if (vox.id == BLOCK_VOID && should_keep_previous(gui)) {
vox = prevVox;
} else {
prevVox = vox;
}
std::wstringstream stream;
stream << "r:" << vox.state.rotation << " s:"
<< std::bitset<3>(vox.state.segment) << " u:"
@ -148,8 +163,17 @@ std::shared_ptr<UINode> create_debug_panel(
}
}));
panel->add(create_label(gui, [&]() -> std::wstring {
const auto& selection = player.selection;
// TODO: move to xml finally
static CursorSelection prevSelection {};
auto selection = player.selection;
const auto& vox = selection.vox;
if (vox.id == BLOCK_VOID && should_keep_previous(gui)) {
selection = prevSelection;
} else {
prevSelection = selection;
}
if (vox.id == BLOCK_VOID) {
return L"x: - y: - z: -";
}
@ -158,7 +182,16 @@ std::shared_ptr<UINode> create_debug_panel(
L" z: " + std::to_wstring(selection.actualPosition.z);
}));
panel->add(create_label(gui, [&]() {
// TODO: move to xml finally
static entityid_t prevEid = ENTITY_NONE;
auto eid = player.getSelectedEntity();
if (eid == ENTITY_NONE && should_keep_previous(gui)) {
eid = prevEid;
} else {
prevEid = eid;
}
if (eid == ENTITY_NONE) {
return std::wstring {L"entity: -"};
} else if (auto entity = level.entities->get(eid)) {

View file

@ -192,7 +192,7 @@ Hud::Hud(Engine& engine, LevelFrontend& frontend, Player& player)
gui.add(contentAccessPanel);
auto dplotter = std::make_shared<Plotter>(gui, 350, 250, 2000, 16);
dplotter->setGravity(Gravity::bottom_right);
dplotter->setGravity(Gravity::BOTTOM_RIGHT);
dplotter->setInteractive(false);
add(HudElement(HudElementMode::PERMANENT, nullptr, dplotter, true));
@ -469,6 +469,7 @@ void Hud::showExchangeSlot() {
gui,
SlotLayout(-1, glm::vec2(), false, false, nullptr, nullptr, nullptr)
);
exchangeSlot->setId("hud.exchange-slot");
exchangeSlot->bind(exchangeSlotInv->getId(), exchangeSlotInv->getSlot(0), &content);
exchangeSlot->setColor(glm::vec4());
exchangeSlot->setInteractive(false);
@ -756,3 +757,13 @@ void Hud::setAllowPause(bool flag) {
}
allowPause = flag;
}
bool Hud::isOpen(const std::string& layoutid) const {
for (const auto& element : elements) {
auto doc = element.getDocument();
if (doc && doc->getId() == layoutid) {
return true;
}
}
return false;
}

View file

@ -213,6 +213,8 @@ public:
void setAllowPause(bool flag);
bool isOpen(const std::string& layoutid) const;
static bool showGeneratorMinimap;
/// @brief Runtime updating debug visualization texture

View file

@ -11,7 +11,7 @@
#include "graphics/ui/elements/Menu.hpp"
#include "graphics/ui/gui_util.hpp"
#include "interfaces/Task.hpp"
#include "io/engine_paths.hpp"
#include "engine/EnginePaths.hpp"
#include "locale.hpp"
#include "logic/scripting/scripting.hpp"
#include "screens/MenuScreen.hpp"

View file

@ -6,6 +6,7 @@
#include "core_defs.hpp"
#include "debug/Logger.hpp"
#include "engine/Engine.hpp"
#include "engine/EnginePaths.hpp"
#include "assets/Assets.hpp"
#include "frontend/ContentGfxCache.hpp"
#include "frontend/LevelFrontend.hpp"
@ -75,13 +76,14 @@ LevelScreen::LevelScreen(
engine, *controller, *renderer, assets, *player
);
keepAlive(settings.graphics.backlight.observe([=](bool) {
player->chunks->saveAndClear();
renderer->clear();
}));
keepAlive(settings.graphics.denseRender.observe([=](bool) {
auto resetChunks = [=](bool) {
player->chunks->saveAndClear();
renderer->clear();
};
keepAlive(settings.graphics.backlight.observe(resetChunks));
keepAlive(settings.graphics.softLighting.observe(resetChunks));
keepAlive(settings.graphics.denseRender.observe([=](bool flag) {
resetChunks(flag);
frontend->getContentGfxCache().refresh();
}));
keepAlive(settings.camera.fov.observe([=](double value) {
@ -107,6 +109,7 @@ LevelScreen::~LevelScreen() {
scripting::on_frontend_close();
// unblock all bindings
input.getBindings().enableAll();
playerController->getPlayer()->chunks->saveAndClear();
controller->onWorldQuit();
engine.getPaths().setCurrentWorldFolder("");
}
@ -276,5 +279,6 @@ void LevelScreen::onEngineShutdown() {
if (hud->isInventoryOpen()) {
hud->closeInventory();
}
controller->processBeforeQuit();
controller->saveWorld();
}

View file

@ -12,9 +12,11 @@
#include "window/Camera.hpp"
#include "engine/Engine.hpp"
MenuScreen::MenuScreen(Engine& engine) : Screen(engine) {
uicamera =
std::make_unique<Camera>(glm::vec3(), engine.getWindow().getSize().y);
MenuScreen::MenuScreen(Engine& engine)
: Screen(engine),
uicamera(
std::make_unique<Camera>(glm::vec3(), engine.getWindow().getSize().y)
) {
uicamera->perspective = false;
uicamera->near = -1.0f;
uicamera->far = 1.0f;
@ -24,7 +26,7 @@ MenuScreen::MenuScreen(Engine& engine) : Screen(engine) {
MenuScreen::~MenuScreen() = default;
void MenuScreen::onOpen() {
engine.getContentControl().resetContent();
engine.getContentControl().resetContent({});
auto menu = engine.getGUI().getMenu();
menu->reset();

View file

@ -49,6 +49,14 @@ ImageData* Atlas::getImage() const {
return image.get();
}
std::shared_ptr<Texture> Atlas::shareTexture() const {
return texture;
}
std::shared_ptr<ImageData> Atlas::shareImageData() const {
return image;
}
void AtlasBuilder::add(const std::string& name, std::unique_ptr<ImageData> image) {
entries.push_back(atlasentry{name, std::shared_ptr<ImageData>(image.release())});
names.insert(name);

View file

@ -14,8 +14,8 @@ class ImageData;
class Texture;
class Atlas {
std::unique_ptr<Texture> texture;
std::unique_ptr<ImageData> image;
std::shared_ptr<Texture> texture;
std::shared_ptr<ImageData> image;
std::unordered_map<std::string, UVRegion> regions;
public:
/// @param image atlas raster
@ -36,6 +36,9 @@ public:
Texture* getTexture() const;
ImageData* getImage() const;
std::shared_ptr<Texture> shareTexture() const;
std::shared_ptr<ImageData> shareImageData() const;
};
struct atlasentry {

View file

@ -95,6 +95,17 @@ void ImageData::blit(const ImageData& image, int x, int y) {
throw std::runtime_error("mismatching format");
}
std::unique_ptr<ImageData> ImageData::cropped(int x, int y, int width, int height) const {
width = std::min<int>(width, this->width - x);
height = std::min<int>(height, this->height - y);
if (width <= 0 || height <= 0) {
throw std::runtime_error("invalid crop dimensions");
}
auto subImage = std::make_unique<ImageData>(format, width, height);
subImage->blitMatchingFormat(*this, -x, -y);
return subImage;
}
static bool clip_line(int& x1, int& y1, int& x2, int& y2, int width, int height) {
const int left = 0;
const int right = width;
@ -409,6 +420,125 @@ void ImageData::fixAlphaColor() {
}
}
static void check_matching(const ImageData& a, const ImageData& b) {
if (b.getWidth() != a.getWidth() ||
b.getHeight() != a.getHeight() ||
b.getFormat() != a.getFormat()) {
throw std::runtime_error("image sizes or formats do not match");
}
}
void ImageData::mulColor(const glm::ivec4& color) {
uint comps;
switch (format) {
case ImageFormat::rgb888: comps = 3; break;
case ImageFormat::rgba8888: comps = 4; break;
default:
throw std::runtime_error("only unsigned byte formats supported");
}
for (uint y = 0; y < height; y++) {
for (uint x = 0; x < width; x++) {
uint idx = (y * width + x) * comps;
for (uint c = 0; c < comps; c++) {
float val = static_cast<float>(data[idx + c]) * color[c] / 255.0f;
data[idx + c] =
static_cast<ubyte>(std::min(std::max(val, 0.0f), 255.0f));
}
}
}
}
void ImageData::addColor(const ImageData& other, int multiplier) {
check_matching(*this, other);
uint comps;
switch (format) {
case ImageFormat::rgb888: comps = 3; break;
case ImageFormat::rgba8888: comps = 4; break;
default:
throw std::runtime_error("only unsigned byte formats supported");
}
for (uint y = 0; y < height; y++) {
for (uint x = 0; x < width; x++) {
uint idx = (y * width + x) * comps;
for (uint c = 0; c < comps; c++) {
int val = data[idx + c] + other.data[idx + c] * multiplier;
data[idx + c] =
static_cast<ubyte>(std::min(std::max(val, 0), 255));
}
}
}
}
void ImageData::extend(int newWidth, int newHeight) {
size_t comps;
switch (format) {
case ImageFormat::rgb888: comps = 3; break;
case ImageFormat::rgba8888: comps = 4; break;
default:
throw std::runtime_error("only unsigned byte formats supported");
}
auto newData = std::make_unique<ubyte[]>(newWidth * newHeight * comps);
for (uint y = 0; y < newHeight; y++) {
for (uint x = 0; x < newWidth; x++) {
for (size_t c = 0; c < comps; c++) {
if (x < width && y < height) {
newData[(y * newWidth + x) * comps + c] =
data[(y * width + x) * comps + c];
} else {
newData[(y * newWidth + x) * comps + c] = 0;
}
}
}
}
data = std::move(newData);
width = newWidth;
height = newHeight;
}
void ImageData::addColor(const glm::ivec4& color, int multiplier) {
uint comps;
switch (format) {
case ImageFormat::rgb888: comps = 3; break;
case ImageFormat::rgba8888: comps = 4; break;
default:
throw std::runtime_error("only unsigned byte formats supported");
}
for (uint y = 0; y < height; y++) {
for (uint x = 0; x < width; x++) {
uint idx = (y * width + x) * comps;
for (uint c = 0; c < comps; c++) {
int val = data[idx + c] + color[c] * multiplier;
data[idx + c] =
static_cast<ubyte>(std::min(std::max(val, 0), 255));
}
}
}
}
void ImageData::mulColor(const ImageData& other) {
check_matching(*this, other);
uint comps;
switch (format) {
case ImageFormat::rgb888: comps = 3; break;
case ImageFormat::rgba8888: comps = 4; break;
default:
throw std::runtime_error("only unsigned byte formats supported");
}
for (uint y = 0; y < height; y++) {
for (uint x = 0; x < width; x++) {
uint idx = (y * width + x) * comps;
for (uint c = 0; c < comps; c++) {
float val = static_cast<float>(data[idx + c]) *
static_cast<float>(other.data[idx + c]) / 255.0f;
data[idx + c] =
static_cast<ubyte>(std::min(std::max(val, 0.0f), 255.0f));
}
}
}
}
std::unique_ptr<ImageData> add_atlas_margins(ImageData* image, int grid_size) {
// RGBA is only supported
assert(image->getFormat() == ImageFormat::rgba8888);

View file

@ -32,6 +32,13 @@ public:
void blit(const ImageData& image, int x, int y);
void extrude(int x, int y, int w, int h);
void fixAlphaColor();
void mulColor(const glm::ivec4& color);
void mulColor(const ImageData& other);
void addColor(const glm::ivec4& color, int multiplier);
void addColor(const ImageData& other, int multiplier);
void extend(int newWidth, int newHeight);
std::unique_ptr<ImageData> cropped(int x, int y, int width, int height) const;
ubyte* getData() const {
return data.get();

View file

@ -44,13 +44,21 @@ void Texture::unbind() const {
void Texture::reload(const ImageData& image) {
width = image.getWidth();
height = image.getHeight();
reload(image.getData());
reload(image.getData(), width, height);
}
void Texture::reload(const ubyte* data) {
void Texture::reload(const ubyte* data, uint width, uint height) {
glBindTexture(GL_TEXTURE_2D, id);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0,
GL_RGBA, GL_UNSIGNED_BYTE, static_cast<const GLvoid*>(data));
glGenerateMipmap(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, 0);
}
void Texture::reloadPartial(const ImageData& image, uint x, uint y, uint w, uint h) {
glBindTexture(GL_TEXTURE_2D, id);
glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, w, h, GL_RGBA, GL_UNSIGNED_BYTE, image.getData());
glGenerateMipmap(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, 0);
}

View file

@ -18,7 +18,8 @@ public:
virtual void bind() const;
virtual void unbind() const;
void reload(const ubyte* data);
void reload(const ubyte* data, uint w, uint h);
void reloadPartial(const ImageData& image, uint x, uint y, uint w, uint h);
void setNearestFilter();

View file

@ -67,7 +67,7 @@ std::unique_ptr<ImageData> BlocksPreview::draw(
glm::vec3 poff = glm::vec3(0.0f, 0.0f, 1.0f);
offset.y += (1.0f - hitbox).y * 0.5f;
shader.uniformMatrix("u_apply", glm::translate(glm::mat4(1.0f), offset));
const auto& model = cache.getModel(def.rt.id);
const auto& model = cache.getModel(def.rt.id, 0);
for (const auto& mesh : model.meshes) {
for (const auto& vertex : mesh.vertices) {
@ -137,5 +137,5 @@ std::unique_ptr<Atlas> BlocksPreview::build(
builder.add(def.name, draw(cache, shader, fbo, batch, def, iconSize));
}
fbo.unbind();
return builder.build(2);
return builder.build(ATLAS_EXTRUSION);
}

View file

@ -9,7 +9,7 @@
#include "lighting/Lightmap.hpp"
#include "frontend/ContentGfxCache.hpp"
const glm::vec3 BlocksRenderer::SUN_VECTOR(0.528265f, 0.833149f, -0.163704f);
const glm::vec3 BlocksRenderer::SUN_VECTOR(0.528265, 0.833149, -0.163704);
const float DIRECTIONAL_LIGHT_FACTOR = 0.3f;
BlocksRenderer::BlocksRenderer(
@ -35,8 +35,7 @@ BlocksRenderer::BlocksRenderer(
blockDefsCache = content.getIndices()->blocks.getDefs();
}
BlocksRenderer::~BlocksRenderer() {
}
BlocksRenderer::~BlocksRenderer() = default;
/// Basic vertex add method
void BlocksRenderer::vertex(
@ -308,7 +307,7 @@ void BlocksRenderer::blockCustomModel(
Z = orient.axes[2];
}
const auto& model = cache.getModel(block.rt.id);
const auto& model = cache.getModel(block.rt.id, block.getVariantIndex(states.userbits));
for (const auto& mesh : model.meshes) {
if (vertexCount + mesh.vertices.size() >= capacity) {
overflow = true;
@ -480,6 +479,7 @@ void BlocksRenderer::render(
) {
bool denseRender = this->denseRender;
bool densePass = this->densePass;
bool enableAO = settings.graphics.softLighting.get();
for (const auto drawGroup : *content.drawGroups) {
int begin = beginEnds[drawGroup][0];
if (begin == 0) {
@ -516,7 +516,7 @@ void BlocksRenderer::render(
switch (def.getModel(state.userbits).type) {
case BlockModelType::BLOCK:
blockCube({x, y, z}, texfaces, def, vox.state, !def.shadeless,
def.ambientOcclusion);
def.ambientOcclusion && enableAO);
break;
case BlockModelType::XSPRITE: {
if (!denseRender)
@ -527,7 +527,7 @@ void BlocksRenderer::render(
case BlockModelType::AABB: {
if (!denseRender)
blockAABB({x, y, z}, texfaces, &def, vox.state.rotation,
!def.shadeless, def.ambientOcclusion);
!def.shadeless, def.ambientOcclusion && enableAO);
break;
}
case BlockModelType::CUSTOM: {
@ -537,7 +537,7 @@ void BlocksRenderer::render(
def,
vox.state,
!def.shadeless,
def.ambientOcclusion
def.ambientOcclusion && enableAO
);
break;
}
@ -561,6 +561,7 @@ SortingMeshData BlocksRenderer::renderTranslucent(
size_t totalSize = 0;
bool densePass = this->densePass;
bool enableAO = settings.graphics.softLighting.get();
for (const auto drawGroup : *content.drawGroups) {
int begin = beginEnds[drawGroup][0];
if (begin == 0) {
@ -594,7 +595,7 @@ SortingMeshData BlocksRenderer::renderTranslucent(
switch (def.getModel(state.userbits).type) {
case BlockModelType::BLOCK:
blockCube({x, y, z}, texfaces, def, vox.state, !def.shadeless,
def.ambientOcclusion);
def.ambientOcclusion && enableAO);
break;
case BlockModelType::XSPRITE: {
blockXSprite(x, y, z, glm::vec3(1.0f),
@ -602,13 +603,24 @@ SortingMeshData BlocksRenderer::renderTranslucent(
break;
}
case BlockModelType::AABB: {
blockAABB({x, y, z}, texfaces, &def, vox.state.rotation,
!def.shadeless, def.ambientOcclusion);
blockAABB(
{x, y, z},
texfaces,
&def,
vox.state.rotation,
!def.shadeless,
def.ambientOcclusion && enableAO
);
break;
}
case BlockModelType::CUSTOM: {
blockCustomModel({x, y, z}, def, vox.state,
!def.shadeless, def.ambientOcclusion);
blockCustomModel(
{x, y, z},
def,
vox.state,
!def.shadeless,
def.ambientOcclusion && enableAO
);
break;
}
default:

View file

@ -19,6 +19,7 @@
#include "logic/LevelController.hpp"
#include "util/stringutil.hpp"
#include "engine/Engine.hpp"
#include "engine/EnginePaths.hpp"
#include "io/io.hpp"
#include "audio/audio.hpp"
#include "maths/util.hpp"

View file

@ -30,14 +30,14 @@ namespace gui {
glm::vec2 size,
glm::vec4 padding = glm::vec4(0.0f),
float interval = 2.0f,
Orientation orientation = Orientation::vertical
Orientation orientation = Orientation::VERTICAL
)
: Container(gui, std::move(size)),
padding(std::move(padding)),
interval(interval) {
}
Orientation orientation = Orientation::vertical;
Orientation orientation = Orientation::VERTICAL;
glm::vec4 padding;
float interval = 2.0f;
};

View file

@ -31,7 +31,7 @@ Button::Button(
GUI& gui,
const std::wstring& text,
glm::vec4 padding,
const onaction& action,
const OnAction& action,
glm::vec2 size
)
: Panel(gui, size, padding, 0.0f) {
@ -42,12 +42,12 @@ Button::Button(
}
if (action) {
listenAction(action);
listenClick(action);
}
setScrollable(false);
label = std::make_shared<Label>(gui, text);
label->setAlign(Align::center);
label->setAlign(Align::CENTER);
label->setSize(getContentSize());
label->setInteractive(false);
add(label);
@ -95,5 +95,5 @@ Align Button::getTextAlign() const {
if (label) {
return label->getAlign();
}
return Align::left;
return Align::LEFT;
}

View file

@ -19,7 +19,7 @@ namespace gui {
GUI& gui,
const std::wstring& text,
glm::vec4 padding,
const onaction& action,
const OnAction& action,
glm::vec2 size = glm::vec2(-1)
);

View file

@ -4,11 +4,11 @@
#include "graphics/core/DrawContext.hpp"
#include "graphics/core/Texture.hpp"
gui::Canvas::Canvas(GUI& gui, ImageFormat inFormat, glm::uvec2 inSize)
: UINode(gui, inSize) {
auto data = std::make_shared<ImageData>(inFormat, inSize.x, inSize.y);
mTexture = Texture::from(data.get());
mData = std::move(data);
gui::Canvas::Canvas(GUI& gui, ImageFormat format, glm::uvec2 size)
: UINode(gui, size) {
auto data = std::make_shared<ImageData>(format, size.x, size.y);
texture = Texture::from(data.get());
this->data = std::move(data);
}
void gui::Canvas::draw(const DrawContext& pctx, const Assets& assets) {
@ -16,6 +16,12 @@ void gui::Canvas::draw(const DrawContext& pctx, const Assets& assets) {
auto col = calcColor();
auto batch = pctx.getBatch2D();
batch->texture(mTexture.get());
batch->texture(texture.get());
batch->rect(pos.x, pos.y, size.x, size.y, 0, 0, 0, {}, false, false, col);
}
void gui::Canvas::setSize(const glm::vec2& size) {
UINode::setSize(size);
data->extend(size.x, size.y);
texture->reload(*data);
}

View file

@ -9,21 +9,23 @@ class Texture;
namespace gui {
class Canvas final : public UINode {
public:
explicit Canvas(GUI& gui, ImageFormat inFormat, glm::uvec2 inSize);
explicit Canvas(GUI& gui, ImageFormat format, glm::uvec2 size);
~Canvas() override = default;
void draw(const DrawContext& pctx, const Assets& assets) override;
[[nodiscard]] auto texture() const {
return mTexture;
void setSize(const glm::vec2& size) override;
[[nodiscard]] auto getTexture() const {
return texture;
}
[[nodiscard]] auto data() const {
return mData;
[[nodiscard]] auto getData() const {
return data;
}
private:
std::shared_ptr<::Texture> mTexture;
std::shared_ptr<ImageData> mData;
std::shared_ptr<::Texture> texture;
std::shared_ptr<ImageData> data;
};
}

View file

@ -52,7 +52,7 @@ FullCheckBox::FullCheckBox(
checkbox(std::make_shared<CheckBox>(gui, checked)),
label(std::make_shared<Label>(gui, text)) {
setColor(glm::vec4(0.0f));
setOrientation(Orientation::horizontal);
setOrientation(Orientation::HORIZONTAL);
add(checkbox);

View file

@ -64,7 +64,7 @@ namespace gui {
checkbox->setTooltip(text);
}
virtual void setSize(glm::vec2 new_size) override {
virtual void setSize(const glm::vec2& new_size) override {
Panel::setSize(new_size);
checkbox->setSize(glm::vec2(size.y, size.y));
}

View file

@ -2,6 +2,8 @@
#include "graphics/core/DrawContext.hpp"
#include "graphics/core/Batch2D.hpp"
#include "window/Window.hpp"
#include "../GUI.hpp"
#include <algorithm>
#include <utility>
@ -77,6 +79,10 @@ void Container::act(float delta) {
node->act(delta);
}
}
if (!intervalEvents.empty()) {
// TODO: make it interval-based
gui.getWindow().setShouldRefresh();
}
for (IntervalEvent& event : intervalEvents) {
event.timer += delta;
if (event.timer > event.interval) {
@ -170,6 +176,7 @@ void Container::add(const std::shared_ptr<UINode>& node) {
parent->setMustRefresh();
parent = parent->getParent();
}
gui.getWindow().setShouldRefresh();
}
void Container::add(const std::shared_ptr<UINode>& node, glm::vec2 pos) {
@ -203,11 +210,11 @@ void Container::clear() {
refresh();
}
void Container::listenInterval(float interval, ontimeout callback, int repeat) {
void Container::listenInterval(float interval, OnTimeOut callback, int repeat) {
intervalEvents.push_back({std::move(callback), interval, 0.0f, repeat});
}
void Container::setSize(glm::vec2 size) {
void Container::setSize(const glm::vec2& size) {
if (size == getSize()) {
return;
}

View file

@ -36,9 +36,11 @@ namespace gui {
virtual void remove(const std::string& id);
virtual void scrolled(int value) override;
virtual void setScrollable(bool flag);
void listenInterval(float interval, ontimeout callback, int repeat=-1);
virtual glm::vec2 getContentOffset() override {return glm::vec2(0.0f, scroll);};
virtual void setSize(glm::vec2 size) override;
void listenInterval(float interval, OnTimeOut callback, int repeat=-1);
virtual glm::vec2 getContentOffset() const override {
return glm::vec2(0.0f, scroll);
};
virtual void setSize(const glm::vec2& size) override;
virtual int getScrollStep() const;
virtual void setScrollStep(int step);
virtual void refresh() override;

View file

@ -36,15 +36,14 @@ void InlineFrame::setDocument(const std::shared_ptr<UiDocument>& document) {
}
void InlineFrame::act(float delta) {
Container::act(delta);
if (document || src.empty()) {
return;
if (document == nullptr && !src.empty()) {
const auto& assets = *gui.getEngine().getAssets();
setDocument(assets.getShared<UiDocument>(src));
}
const auto& assets = *gui.getEngine().getAssets();
setDocument(assets.getShared<UiDocument>(src));
Container::act(delta);
}
void InlineFrame::setSize(glm::vec2 size) {
void InlineFrame::setSize(const glm::vec2& size) {
Container::setSize(size);
if (root) {
root->setSize(size);

View file

@ -5,7 +5,7 @@
class UiDocument;
namespace gui {
class InlineFrame : public Container {
class InlineFrame final : public Container {
public:
explicit InlineFrame(GUI& gui);
virtual ~InlineFrame();
@ -14,7 +14,7 @@ namespace gui {
void setDocument(const std::shared_ptr<UiDocument>& document);
void act(float delta) override;
void setSize(glm::vec2 size) override;
void setSize(const glm::vec2& size) override;
const std::string& getSrc() const;
private:

View file

@ -5,7 +5,7 @@
namespace gui {
class Label;
class InputBindBox : public Panel {
class InputBindBox final : public Panel {
protected:
Binding& binding;
glm::vec4 focusedColor {0.1f, 0.15f, 0.35f, 0.75f};
@ -15,13 +15,13 @@ namespace gui {
GUI& gui, Binding& binding, glm::vec4 padding = glm::vec4(6.0f)
);
virtual void drawBackground(
void drawBackground(
const DrawContext& pctx, const Assets& assets
) override;
virtual void clicked(Mousecode button) override;
virtual void keyPressed(Keycode key) override;
virtual bool isFocuskeeper() const override {
void clicked(Mousecode button) override;
void keyPressed(Keycode key) override;
bool isFocuskeeper() const override {
return true;
}
};

View file

@ -528,14 +528,14 @@ void InventoryView::setSelected(int index) {
}
}
void InventoryView::setPos(glm::vec2 pos) {
void InventoryView::setPos(const glm::vec2& pos) {
Container::setPos(pos - origin);
}
void InventoryView::setOrigin(glm::vec2 origin) {
void InventoryView::setOrigin(const glm::vec2& origin) {
this->origin = origin;
}
glm::vec2 InventoryView::getOrigin() const {
const glm::vec2& InventoryView::getOrigin() const {
return origin;
}

View file

@ -109,7 +109,7 @@ namespace gui {
static inline std::string EXCHANGE_SLOT_NAME = "exchange-slot";
};
class InventoryView : public gui::Container {
class InventoryView final : public gui::Container {
const Content* content = nullptr;
std::shared_ptr<Inventory> inventory;
@ -120,10 +120,10 @@ namespace gui {
InventoryView(GUI& gui);
virtual ~InventoryView();
virtual void setPos(glm::vec2 pos) override;
void setPos(const glm::vec2& pos) override;
void setOrigin(glm::vec2 origin);
glm::vec2 getOrigin() const;
void setOrigin(const glm::vec2& origin);
const glm::vec2& getOrigin() const;
void setSelected(int index);

View file

@ -230,14 +230,14 @@ void Label::draw(const DrawContext& pctx, const Assets& assets) {
glm::vec2 pos = calcPos();
switch (align) {
case Align::left: break;
case Align::center: pos.x += (size.x-newsize.x)*0.5f; break;
case Align::right: pos.x += size.x-newsize.x; break;
case Align::LEFT: break;
case Align::CENTER: pos.x += (size.x-newsize.x)*0.5f; break;
case Align::RIGHT: pos.x += size.x-newsize.x; break;
}
switch (valign) {
case Align::top: break;
case Align::center: pos.y += (size.y-newsize.y)*0.5f; break;
case Align::bottom: pos.y += size.y-newsize.y; break;
case Align::TOP: break;
case Align::CENTER: pos.y += (size.y-newsize.y)*0.5f; break;
case Align::BOTTOM: pos.y += size.y-newsize.y; break;
}
textYOffset = pos.y-calcPos().y;
totalLineHeight = lineHeight;

View file

@ -44,7 +44,7 @@ namespace gui {
float lineInterval = 1.5f;
/// @brief Vertical alignment (only when multiline is set to false)
Align valign = Align::center;
Align valign = Align::CENTER;
/// @brief Line separators and wrapping will be ignored if set to false
bool multiline = false;

View file

@ -72,7 +72,7 @@ void Panel::refresh() {
float x = padding.x;
float y = padding.y;
glm::vec2 size = getSize();
if (orientation == Orientation::vertical) {
if (orientation == Orientation::VERTICAL) {
float maxw = size.x;
for (auto& node : nodes) {
const glm::vec4 margin = node->getMargin();

View file

@ -19,7 +19,7 @@ SelectBox::SelectBox(
: Button(gui, selected.text, padding, nullptr, glm::vec2(contentWidth, -1)),
options(std::move(options)) {
listenAction([this](GUI& gui) {
listenClick([this](GUI& gui) {
auto panel = std::make_shared<Panel>(gui, getSize());
panel->setPos(calcPos() + glm::vec2(0, size.y));
for (const auto& option : this->options) {
@ -41,7 +41,7 @@ SelectBox::SelectBox(
});
}
void SelectBox::listenChange(onstringchange&& callback) {
void SelectBox::listenChange(OnStringChange&& callback) {
changeCallbacks.listen(std::move(callback));
}

View file

@ -24,7 +24,7 @@ namespace gui {
const glm::vec4& padding
);
void listenChange(onstringchange&& callback);
void listenChange(OnStringChange&& callback);
void setSelected(const Option& selected);

View file

@ -7,7 +7,7 @@ using namespace gui;
SplitBox::SplitBox(GUI& gui, const glm::vec2& size, float splitPos, Orientation orientation)
: BasePanel(gui, size, glm::vec4(), 4.0f, orientation), splitPos(splitPos) {
setCursor(
orientation == Orientation::vertical ? CursorShape::NS_RESIZE
orientation == Orientation::VERTICAL ? CursorShape::NS_RESIZE
: CursorShape::EW_RESIZE
);
}
@ -17,7 +17,7 @@ void SplitBox::mouseMove(int x, int y) {
auto size = getSize();
glm::ivec2 cursor(x - pos.x, y - pos.y);
int axis = orientation == Orientation::vertical;
int axis = orientation == Orientation::VERTICAL;
int v = cursor[axis];
v = std::max(std::min(static_cast<int>(size[axis]) - 10, v), 10);
@ -61,7 +61,7 @@ void SplitBox::refresh() {
nodeA->setPos(glm::vec2(padding));
const auto& p = padding;
if (orientation == Orientation::vertical) {
if (orientation == Orientation::VERTICAL) {
float splitPos = this->splitPos * size.y;
nodeA->setSize({size.x-p.x-p.z, splitPos - sepRadius - p.y});
nodeB->setSize({size.x-p.x-p.z, size.y - splitPos - sepRadius - p.w});

View file

@ -216,7 +216,7 @@ TextBox::TextBox(GUI& gui, std::wstring placeholder, glm::vec4 padding)
lineNumbersLabel->setSize(
size - glm::vec2(padding.z + padding.x, padding.w + padding.y)
);
lineNumbersLabel->setVerticalAlign(Align::top);
lineNumbersLabel->setVerticalAlign(Align::TOP);
add(lineNumbersLabel);
setHoverColor(glm::vec4(0.05f, 0.1f, 0.2f, 0.75f));
@ -231,7 +231,7 @@ TextBox::~TextBox() = default;
void TextBox::draw(const DrawContext& pctx, const Assets& assets) {
Container::draw(pctx, assets);
if (!isFocused()) {
if (!isFocused() && !keepLineSelection) {
return;
}
const auto& labelText = getText();
@ -252,7 +252,7 @@ void TextBox::draw(const DrawContext& pctx, const Assets& assets) {
float time = gui.getWindow().time();
if (editable && static_cast<int>((time - caretLastMove) * 2) % 2 == 0) {
if (isFocused() && editable && static_cast<int>((time - caretLastMove) * 2) % 2 == 0) {
uint line = label->getLineByTextIndex(caret);
uint lcaret = caret - label->getTextLineOffset(line);
int width = rawTextCache.metrics.calcWidth(input, 0, lcaret);
@ -308,42 +308,44 @@ void TextBox::draw(const DrawContext& pctx, const Assets& assets) {
}
}
if (isFocused() && multiline) {
auto selectionCtx = subctx.sub(batch);
selectionCtx.setBlendMode(BlendMode::addition);
batch->setColor(glm::vec4(1, 1, 1, 0.1f));
uint line = label->getLineByTextIndex(caret);
while (label->isFakeLine(line)) {
line--;
}
do {
int lineY = label->getLineYOffset(line);
batch->setColor(glm::vec4(1, 1, 1, 0.05f));
if (showLineNumbers) {
batch->rect(
lcoord.x - 8,
lcoord.y + lineY,
label->getSize().x,
lineHeight
);
batch->setColor(glm::vec4(1, 1, 1, 0.10f));
batch->rect(
lcoord.x - LINE_NUMBERS_PANE_WIDTH,
lcoord.y + lineY,
LINE_NUMBERS_PANE_WIDTH - 8,
lineHeight
);
} else {
batch->rect(
lcoord.x, lcoord.y + lineY, label->getSize().x, lineHeight
);
}
line++;
} while (line < label->getLinesNumber() && label->isFakeLine(line));
if (!multiline) {
return;
}
auto selectionCtx = subctx.sub(batch);
selectionCtx.setBlendMode(BlendMode::addition);
batch->setColor(glm::vec4(1, 1, 1, 0.1f));
uint line = label->getLineByTextIndex(caret);
while (label->isFakeLine(line)) {
line--;
}
do {
int lineY = label->getLineYOffset(line);
batch->setColor(glm::vec4(1, 1, 1, 0.05f));
if (showLineNumbers) {
batch->rect(
lcoord.x - 8,
lcoord.y + lineY,
label->getSize().x,
lineHeight
);
batch->setColor(glm::vec4(1, 1, 1, 0.10f));
batch->rect(
lcoord.x - LINE_NUMBERS_PANE_WIDTH,
lcoord.y + lineY,
LINE_NUMBERS_PANE_WIDTH - 8,
lineHeight
);
} else {
batch->rect(
lcoord.x, lcoord.y + lineY, label->getSize().x, lineHeight
);
}
line++;
} while (line < label->getLinesNumber() && label->isFakeLine(line));
}
void TextBox::drawBackground(const DrawContext& pctx, const Assets& assets) {
@ -484,7 +486,7 @@ void TextBox::erase(size_t start, size_t length) {
setCaret(caret - length);
}
auto left = input.substr(0, start);
auto right = input.substr(end);
auto right = end >= input.length() ? L"" : input.substr(end);
input = left + right;
}
@ -565,7 +567,7 @@ bool TextBox::isValid() const {
void TextBox::setMultiline(bool multiline) {
this->multiline = multiline;
label->setMultiline(multiline);
label->setVerticalAlign(multiline ? Align::top : Align::center);
label->setVerticalAlign(multiline ? Align::TOP : Align::CENTER);
}
bool TextBox::isMultiline() const {
@ -605,6 +607,14 @@ size_t TextBox::getSelectionEnd() const {
return selectionEnd;
}
void TextBox::setKeepLineSelection(bool flag) {
keepLineSelection = flag;
}
bool TextBox::isKeepLineSelection() const {
return keepLineSelection;
}
void TextBox::setOnEditStart(runnable oneditstart) {
onEditStart = oneditstart;
}
@ -678,6 +688,11 @@ int TextBox::calcIndexAt(int x, int y) const {
);
}
int TextBox::getLineYOffset(int line) const {
if (rawTextCache.fontId == 0) return 0;
return label->getLineYOffset(line);
}
static inline std::wstring get_alphabet(wchar_t c) {
std::wstring alphabet {c};
if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_') {
@ -1041,11 +1056,11 @@ void TextBox::keyPressed(Keycode key) {
resetSelection();
}
}
if (key == Keycode::Z) {
if (editable && key == Keycode::Z) {
historian->undo();
refreshSyntax();
}
if (key == Keycode::Y) {
if (editable && key == Keycode::Y) {
historian->redo();
refreshSyntax();
}

View file

@ -62,6 +62,7 @@ namespace gui {
bool editable = true;
bool autoresize = false;
bool showLineNumbers = false;
bool keepLineSelection = false;
std::string markup;
std::string syntax;
@ -74,7 +75,6 @@ namespace gui {
size_t normalizeIndex(int index);
int calcIndexAt(int x, int y) const;
void setTextOffset(uint x);
bool eraseSelected();
void resetSelection();
@ -186,6 +186,9 @@ namespace gui {
/// @return line position in text
virtual size_t getLinePos(uint line) const;
int calcIndexAt(int x, int y) const;
int getLineYOffset(int line) const;
/// @brief Check text with validator set with setTextValidator
/// @return true if text is valid
virtual bool validate();
@ -220,6 +223,9 @@ namespace gui {
size_t getSelectionStart() const;
size_t getSelectionEnd() const;
void setKeepLineSelection(bool flag);
bool isKeepLineSelection() const;
/// @brief Set runnable called on textbox focus
virtual void setOnEditStart(runnable oneditstart);

View file

@ -64,41 +64,47 @@ UINode* UINode::getParent() const {
return parent;
}
UINode* UINode::listenAction(const onaction& action) {
actions.listen(action);
return this;
void UINode::listenClick(OnAction action) {
actions.listen(UIAction::CLICK, std::move(action));
}
UINode* UINode::listenDoubleClick(const onaction& action) {
doubleClickCallbacks.listen(action);
return this;
void UINode::listenRightClick(OnAction action) {
actions.listen(UIAction::RIGHT_CLICK, std::move(action));
}
UINode* UINode::listenFocus(const onaction& action) {
focusCallbacks.listen(action);
return this;
void UINode::listenDoubleClick(OnAction action) {
actions.listen(UIAction::DOUBLE_CLICK, std::move(action));
}
UINode* UINode::listenDefocus(const onaction& action) {
defocusCallbacks.listen(action);
return this;
void UINode::listenFocus(OnAction action) {
actions.listen(UIAction::FOCUS, std::move(action));
}
void UINode::listenDefocus(OnAction action) {
actions.listen(UIAction::DEFOCUS, std::move(action));
}
void UINode::click(int, int) {
pressed = true;
}
void UINode::clicked(Mousecode button) {
if (button == Mousecode::BUTTON_2) {
actions.notify(UIAction::RIGHT_CLICK, gui);
}
}
void UINode::doubleClick(int x, int y) {
pressed = true;
if (isInside(glm::vec2(x, y))) {
doubleClickCallbacks.notify(gui);
actions.notify(UIAction::DOUBLE_CLICK, gui);
}
}
void UINode::mouseRelease(int x, int y) {
pressed = false;
if (isInside(glm::vec2(x, y))) {
actions.notify(gui);
actions.notify(UIAction::CLICK, gui);
}
}
@ -108,12 +114,12 @@ bool UINode::isPressed() const {
void UINode::onFocus() {
focused = true;
focusCallbacks.notify(gui);
actions.notify(UIAction::FOCUS, gui);
}
void UINode::defocus() {
focused = false;
defocusCallbacks.notify(gui);
actions.notify(UIAction::DEFOCUS, gui);
}
bool UINode::isFocused() const {
@ -197,7 +203,7 @@ glm::vec4 UINode::calcColor() const {
return color;
}
void UINode::setPos(glm::vec2 pos) {
void UINode::setPos(const glm::vec2& pos) {
this->pos = pos;
}
@ -209,7 +215,7 @@ glm::vec2 UINode::getSize() const {
return size;
}
void UINode::setSize(glm::vec2 size) {
void UINode::setSize(const glm::vec2& size) {
this->size = glm::vec2(
glm::max(minSize.x, glm::min(maxSize.x, size.x)),
glm::max(minSize.y, glm::min(maxSize.y, size.y))
@ -220,7 +226,7 @@ glm::vec2 UINode::getMinSize() const {
return minSize;
}
void UINode::setMinSize(glm::vec2 minSize) {
void UINode::setMinSize(const glm::vec2& minSize) {
this->minSize = minSize;
setSize(getSize());
}
@ -229,7 +235,7 @@ glm::vec2 UINode::getMaxSize() const {
return maxSize;
}
void UINode::setMaxSize(glm::vec2 maxSize) {
void UINode::setMaxSize(const glm::vec2& maxSize) {
this->maxSize = maxSize;
setSize(getSize());
}
@ -334,7 +340,7 @@ void UINode::reposition() {
}
void UINode::setGravity(Gravity gravity) {
if (gravity == Gravity::none) {
if (gravity == Gravity::NONE) {
setPositionFunc(nullptr);
return;
}
@ -349,27 +355,27 @@ void UINode::setGravity(Gravity gravity) {
float x = 0.0f, y = 0.0f;
switch (gravity) {
case Gravity::top_left:
case Gravity::center_left:
case Gravity::bottom_left: x = margin.x; break;
case Gravity::top_center:
case Gravity::center_center:
case Gravity::bottom_center: x = (parentSize.x-size.x)/2.0f; break;
case Gravity::top_right:
case Gravity::center_right:
case Gravity::bottom_right: x = parentSize.x-size.x-margin.z; break;
case Gravity::TOP_LEFT:
case Gravity::CENTER_LEFT:
case Gravity::BOTTOM_LEFT: x = margin.x; break;
case Gravity::TOP_CENTER:
case Gravity::CENTER_CENTER:
case Gravity::BOTTOM_CENTER: x = (parentSize.x-size.x)/2.0f; break;
case Gravity::TOP_RIGHT:
case Gravity::CENTER_RIGHT:
case Gravity::BOTTOM_RIGHT: x = parentSize.x-size.x-margin.z; break;
default: break;
}
switch (gravity) {
case Gravity::top_left:
case Gravity::top_center:
case Gravity::top_right: y = margin.y; break;
case Gravity::center_left:
case Gravity::center_center:
case Gravity::center_right: y = (parentSize.y-size.y)/2.0f; break;
case Gravity::bottom_left:
case Gravity::bottom_center:
case Gravity::bottom_right: y = parentSize.y-size.y-margin.w; break;
case Gravity::TOP_LEFT:
case Gravity::TOP_CENTER:
case Gravity::TOP_RIGHT: y = margin.y; break;
case Gravity::CENTER_LEFT:
case Gravity::CENTER_CENTER:
case Gravity::CENTER_RIGHT: y = (parentSize.y-size.y)/2.0f; break;
case Gravity::BOTTOM_LEFT:
case Gravity::BOTTOM_CENTER:
case Gravity::BOTTOM_RIGHT: y = parentSize.y-size.y-margin.w; break;
default: break;
}
return glm::vec2(x, y);

View file

@ -19,9 +19,9 @@ namespace gui {
class GUI;
class Container;
using onaction = std::function<void(GUI&)>;
using onnumberchange = std::function<void(GUI&, double)>;
using onstringchange = std::function<void(GUI&, const std::string&)>;
using OnAction = std::function<void(GUI&)>;
using OnNumberChange = std::function<void(GUI&, double)>;
using OnStringChange = std::function<void(GUI&, const std::string&)>;
template<typename... Args>
class CallbacksSet {
@ -46,28 +46,60 @@ namespace gui {
}
};
using ActionsSet = CallbacksSet<GUI&>;
template<class TagT, typename... Args>
class TaggedCallbacksSet {
public:
using Func = std::function<void(Args...)>;
private:
std::unique_ptr<std::vector<std::pair<TagT, Func>>> callbacks;
public:
void listen(TagT tag, Func&& callback) {
if (callbacks == nullptr) {
callbacks =
std::make_unique<std::vector<std::pair<TagT, Func>>>();
}
callbacks->push_back({tag, std::move(callback)});
}
void notify(TagT notifyTag, Args&&... args) {
if (callbacks == nullptr) {
return;
}
for (const auto& [tag, callback] : * callbacks) {
if (tag != notifyTag) {
continue;
}
callback(args...);
}
}
};
enum class UIAction {
CLICK, DOUBLE_CLICK, FOCUS, DEFOCUS, RIGHT_CLICK
};
using ActionsSet = TaggedCallbacksSet<UIAction, GUI&>;
using StringCallbacksSet = CallbacksSet<GUI&, const std::string&>;
enum class Align {
left, center, right,
top=left, bottom=right,
LEFT, CENTER, RIGHT,
TOP=LEFT, BOTTOM=RIGHT,
};
enum class Gravity {
none,
NONE,
top_left,
top_center,
top_right,
TOP_LEFT,
TOP_CENTER,
TOP_RIGHT,
center_left,
center_center,
center_right,
CENTER_LEFT,
CENTER_CENTER,
CENTER_RIGHT,
bottom_left,
bottom_center,
bottom_right
BOTTOM_LEFT,
BOTTOM_CENTER,
BOTTOM_RIGHT
};
/// @brief Base abstract class for all UI elements
@ -112,21 +144,15 @@ namespace gui {
/// @brief z-index property specifies the stack order of an element
int zindex = 0;
/// @brief element content alignment (supported by Label only)
Align align = Align::left;
Align align = Align::LEFT;
/// @brief parent element
UINode* parent = nullptr;
/// @brief position supplier for the element (called on parent element size update)
vec2supplier positionfunc = nullptr;
/// @brief size supplier for the element (called on parent element size update)
vec2supplier sizefunc = nullptr;
/// @brief 'onclick' callbacks
/// @brief parameterless callbacks
ActionsSet actions;
/// @brief 'ondoubleclick' callbacks
ActionsSet doubleClickCallbacks;
/// @brief 'onfocus' callbacks
ActionsSet focusCallbacks;
/// @brief 'ondefocus' callbacks
ActionsSet defocusCallbacks;
/// @brief element tooltip text
std::wstring tooltip;
/// @brief element tooltip delay
@ -187,16 +213,17 @@ namespace gui {
/// @brief Get element z-index
int getZIndex() const;
virtual UINode* listenAction(const onaction& action);
virtual UINode* listenDoubleClick(const onaction& action);
virtual UINode* listenFocus(const onaction& action);
virtual UINode* listenDefocus(const onaction& action);
virtual void listenClick(OnAction action);
virtual void listenRightClick(OnAction action);
virtual void listenDoubleClick(OnAction action);
virtual void listenFocus(OnAction action);
virtual void listenDefocus(OnAction action);
virtual void defocus();
virtual void onFocus();
virtual void doubleClick(int x, int y);
virtual void click(int x, int y);
virtual void clicked(Mousecode button) {}
virtual void clicked(Mousecode button);
virtual void mouseMove(int x, int y) {};
virtual void mouseRelease(int x, int y);
virtual void scrolled(int value);
@ -240,17 +267,17 @@ namespace gui {
virtual glm::vec4 calcColor() const;
/// @brief Get inner content offset. Used for scroll
virtual glm::vec2 getContentOffset() {return glm::vec2(0.0f);};
virtual glm::vec2 getContentOffset() const {return glm::vec2(0.0f);};
/// @brief Calculate screen position of the element
virtual glm::vec2 calcPos() const;
virtual void setPos(glm::vec2 pos);
virtual void setPos(const glm::vec2& pos);
virtual glm::vec2 getPos() const;
glm::vec2 getSize() const;
virtual void setSize(glm::vec2 size);
virtual void setSize(const glm::vec2& size);
glm::vec2 getMinSize() const;
virtual void setMinSize(glm::vec2 size);
virtual void setMinSize(const glm::vec2& size);
glm::vec2 getMaxSize() const;
virtual void setMaxSize(glm::vec2 size);
virtual void setMaxSize(const glm::vec2& size);
/// @brief Called in containers when new element added
virtual void refresh() {};
virtual void fullRefresh() {

View file

@ -3,12 +3,12 @@
#include <functional>
namespace gui {
enum class Orientation { vertical, horizontal };
enum class Orientation { VERTICAL, HORIZONTAL };
using ontimeout = std::function<void()>;
using OnTimeOut = std::function<void()>;
struct IntervalEvent {
ontimeout callback;
OnTimeOut callback;
float interval;
float timer;
// -1 - infinity, 1 - one time event

View file

@ -101,7 +101,7 @@ void guiutil::confirm(
gui, glm::vec2(600, 200), glm::vec4(8.0f), 8.0f
);
panel->setGravity(Gravity::center_center);
panel->setGravity(Gravity::CENTER_CENTER);
container->add(panel);
panel->setColor(glm::vec4(0.0f, 0.0f, 0.0f, 0.5f));

View file

@ -30,31 +30,31 @@
using namespace gui;
static Align align_from_string(std::string_view str, Align def) {
if (str == "left") return Align::left;
if (str == "center") return Align::center;
if (str == "right") return Align::right;
if (str == "top") return Align::top;
if (str == "bottom") return Align::bottom;
if (str == "left") return Align::LEFT;
if (str == "center") return Align::CENTER;
if (str == "right") return Align::RIGHT;
if (str == "top") return Align::TOP;
if (str == "bottom") return Align::BOTTOM;
return def;
}
static Gravity gravity_from_string(const std::string& str) {
static const std::unordered_map<std::string, Gravity> gravity_names {
{"top-left", Gravity::top_left},
{"top-center", Gravity::top_center},
{"top-right", Gravity::top_right},
{"center-left", Gravity::center_left},
{"center-center", Gravity::center_center},
{"center-right", Gravity::center_right},
{"bottom-left", Gravity::bottom_left},
{"bottom-center", Gravity::bottom_center},
{"bottom-right", Gravity::bottom_right},
{"top-left", Gravity::TOP_LEFT},
{"top-center", Gravity::TOP_CENTER},
{"top-right", Gravity::TOP_RIGHT},
{"center-left", Gravity::CENTER_LEFT},
{"center-center", Gravity::CENTER_CENTER},
{"center-right", Gravity::CENTER_RIGHT},
{"bottom-left", Gravity::BOTTOM_LEFT},
{"bottom-center", Gravity::BOTTOM_CENTER},
{"bottom-right", Gravity::BOTTOM_RIGHT},
};
auto found = gravity_names.find(str);
if (found != gravity_names.end()) {
return found->second;
}
return Gravity::none;
return Gravity::NONE;
}
static runnable create_runnable(
@ -73,7 +73,7 @@ static runnable create_runnable(
return nullptr;
}
static onaction create_action(
static OnAction create_action(
const UiXmlReader& reader,
const xml::xmlelement& element,
const std::string& name
@ -178,7 +178,11 @@ static void read_uinode(
}
if (auto onclick = create_action(reader, element, "onclick")) {
node.listenAction(onclick);
node.listenClick(onclick);
}
if (auto onclick = create_action(reader, element, "onrightclick")) {
node.listenRightClick(onclick);
}
if (auto onfocus = create_action(reader, element, "onfocus")) {
@ -248,7 +252,7 @@ static void read_base_panel_impl(
if (element.has("orientation")) {
auto& oname = element.attr("orientation").getText();
if (oname == "horizontal") {
panel.setOrientation(Orientation::horizontal);
panel.setOrientation(Orientation::HORIZONTAL);
}
}
}
@ -273,7 +277,7 @@ static void read_panel_impl(
if (element.has("orientation")) {
auto& oname = element.attr("orientation").getText();
if (oname == "horizontal") {
panel.setOrientation(Orientation::horizontal);
panel.setOrientation(Orientation::HORIZONTAL);
}
}
if (subnodes) {
@ -334,7 +338,7 @@ static std::shared_ptr<UINode> read_label(
if (element.has("multiline")) {
label->setMultiline(element.attr("multiline").asBool());
if (!element.has("valign")) {
label->setVerticalAlign(Align::top);
label->setVerticalAlign(Align::TOP);
}
}
if (element.has("text-wrap")) {
@ -360,8 +364,8 @@ static std::shared_ptr<UINode> read_split_box(
float splitPos = element.attr("split-pos", "0.5").asFloat();
Orientation orientation =
element.attr("orientation", "vertical").getText() == "horizontal"
? Orientation::horizontal
: Orientation::vertical;
? Orientation::HORIZONTAL
: Orientation::VERTICAL;
auto splitBox = std::make_shared<SplitBox>(
reader.getGUI(), glm::vec2(), splitPos, orientation
);
@ -569,6 +573,11 @@ static std::shared_ptr<UINode> read_text_box(
if (element.has("line-numbers")) {
textbox->setShowLineNumbers(element.attr("line-numbers").asBool());
}
if (element.has("keep-line-selection")) {
textbox->setKeepLineSelection(
element.attr("keep-line-selection").asBool()
);
}
if (element.has("markup")) {
textbox->setMarkup(element.attr("markup").getText());
}

View file

@ -1,3 +1,5 @@
#pragma once
#include <iostream>
#include <streambuf>
#include <memory>

View file

@ -0,0 +1,220 @@
#include "MemoryDevice.hpp"
#include "../memory_istream.hpp"
#include "../memory_ostream.hpp"
#include "../finalizing_ostream.hpp"
#include <algorithm>
io::MemoryDevice::MemoryDevice() {}
std::filesystem::path io::MemoryDevice::resolve(std::string_view path) {
throw std::runtime_error("unable to resolve filesystem path");
}
std::unique_ptr<std::ostream> io::MemoryDevice::write(std::string_view path) {
std::string filePath = std::string(path);
return std::make_unique<finalizing_ostream>(
std::make_unique<memory_ostream>(),
[this, filePath](auto ostream) {
auto& memoryStream = dynamic_cast<memory_ostream&>(*ostream);
createFile(std::move(filePath), memoryStream.release());
}
);
}
std::unique_ptr<std::istream> io::MemoryDevice::read(std::string_view path) {
const auto& found = nodes.find(std::string(path));
if (found == nodes.end()) {
return nullptr;
}
auto& node = found->second;
if (auto file = node.get_if<File>()) {
if (file->content != nullptr) {
return std::make_unique<memory_view_istream>(file->content);
}
}
return nullptr;
}
size_t io::MemoryDevice::size(std::string_view path) {
const auto& found = nodes.find(std::string(path));
if (found == nodes.end()) {
return 0;
}
return std::visit([](auto&& arg) -> size_t {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, File>) {
return arg.content.size();
} else if constexpr (std::is_same_v<T, Dir>) {
return arg.content.size();
} else {
return 0;
}
}, found->second.data);
}
io::file_time_type io::MemoryDevice::lastWriteTime(std::string_view path) {
return file_time_type::min();
}
bool io::MemoryDevice::exists(std::string_view path) {
if (path.empty()) {
return true;
}
return nodes.find(std::string(path)) != nodes.end();
}
bool io::MemoryDevice::isdir(std::string_view path) {
if (path.empty()) {
return true;
}
const auto& found = nodes.find(std::string(path));
if (found == nodes.end()) {
return false;
}
return found->second.holds_alternative<Dir>();
}
bool io::MemoryDevice::isfile(std::string_view path) {
const auto& found = nodes.find(std::string(path));
if (found == nodes.end()) {
return false;
}
return found->second.holds_alternative<File>();
}
bool io::MemoryDevice::mkdir(std::string_view path) {
return createDir(std::string(path)) != nullptr;
}
bool io::MemoryDevice::mkdirs(std::string_view path) {
io::path dirPath = std::string(path);
std::vector<std::string> parts;
while (!dirPath.pathPart().empty()) {
parts.push_back(dirPath.name());
dirPath = dirPath.parent();
}
for (int i = parts.size() - 1; i >= 0; i--) {
dirPath = dirPath / parts[i];
createDir(dirPath.string());
}
return true;
}
bool io::MemoryDevice::remove(std::string_view path) {
std::string pathString = std::string(path);
const auto& found = nodes.find(pathString);
if (found == nodes.end()) {
return false;
}
if (found->second.holds_alternative<Dir>()) {
const auto& dir = found->second.get_if<Dir>();
if (!dir->content.empty()) {
return false;
}
}
io::path filePath = pathString;
io::path parentPath = filePath.parent();
auto parentDir = getDir(parentPath.string());
if (parentDir) {
auto& content = parentDir->content;
content.erase(
std::remove(content.begin(), content.end(), filePath.name()),
content.end()
);
}
nodes.erase(found);
return true;
}
uint64_t io::MemoryDevice::removeAll(std::string_view path) {
std::string pathString = std::string(path);
const auto& found = nodes.find(pathString);
if (found == nodes.end()) {
return 0;
}
io::path filePath = pathString;
uint64_t count = 0;
if (found->second.holds_alternative<Dir>()) {
auto dir = found->second.get_if<Dir>();
auto files = dir->content;
for (const auto& name : files) {
io::path subPath = filePath / name;
count += removeAll(subPath.string());
}
}
if (remove(pathString)) {
count++;
}
return count;
}
namespace {
struct MemoryPathsGenerator : public io::PathsGenerator {
std::vector<std::string> entries;
size_t index = 0;
MemoryPathsGenerator(std::vector<std::string>&& entries)
: entries(std::move(entries)) {}
bool next(io::path& outPath) override {
if (index >= entries.size()) {
return false;
}
outPath = entries[index++];
return true;
}
};
}
std::unique_ptr<io::PathsGenerator> io::MemoryDevice::list(std::string_view path) {
auto dir = getDir(path);
if (!dir) {
return nullptr;
}
return std::make_unique<MemoryPathsGenerator>(
std::vector<std::string>(dir->content)
);
}
io::MemoryDevice::Dir* io::MemoryDevice::createDir(std::string path) {
io::path filePath = path;
io::path parent = filePath.parent();
auto parentDir = getDir(parent.string());
if (!parentDir) {
return nullptr;
}
parentDir->content.push_back(filePath.name());
auto& node = nodes[std::move(path)];
node.data = Dir {};
return node.get_if<Dir>();
}
io::MemoryDevice::Node* io::MemoryDevice::createFile(
std::string path, util::Buffer<char>&& content
) {
io::path filePath = path;
io::path parent = filePath.parent();
auto dir = getDir(parent.string());
if (!dir) {
return nullptr;
}
dir->content.push_back(filePath.name());
auto& node = nodes[std::move(path)];
node.data = File {std::move(content)};
return &node;
}
io::MemoryDevice::Dir* io::MemoryDevice::getDir(std::string_view path) {
if (path.empty()) {
return &rootDir;
}
const auto& found = nodes.find(std::string(path));
if (found == nodes.end()) {
return nullptr;
}
auto& node = found->second;
return node.get_if<Dir>();
}

View file

@ -0,0 +1,71 @@
#pragma once
#include "Device.hpp"
#include "util/Buffer.hpp"
#include <vector>
#include <variant>
#include <optional>
#include <unordered_map>
namespace io {
/// @brief In-memory filesystem device
class MemoryDevice : public Device {
enum class NodeType {
DIR, FILE
};
struct File {
util::Buffer<char> content;
};
struct Dir {
std::vector<std::string> content;
};
struct Node {
std::variant<Dir, File> data;
NodeType type() const {
return std::visit([](auto&& arg) -> NodeType {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, Dir>) return NodeType::DIR;
else if constexpr (std::is_same_v<T, File>) return NodeType::FILE;
}, data);
}
template <typename T>
bool holds_alternative() const {
return std::holds_alternative<T>(data);
}
template <typename T>
T* get_if() {
return std::get_if<T>(&data);
}
};
public:
MemoryDevice();
std::filesystem::path resolve(std::string_view path) override;
std::unique_ptr<std::ostream> write(std::string_view path) override;
std::unique_ptr<std::istream> read(std::string_view path) override;
size_t size(std::string_view path) override;
file_time_type lastWriteTime(std::string_view path) override;
bool exists(std::string_view path) override;
bool isdir(std::string_view path) override;
bool isfile(std::string_view path) override;
bool mkdir(std::string_view path) override;
bool mkdirs(std::string_view path) override;
bool remove(std::string_view path) override;
uint64_t removeAll(std::string_view path) override;
std::unique_ptr<PathsGenerator> list(std::string_view path) override;
private:
std::unordered_map<std::string, Node> nodes;
Dir rootDir {};
Node* createFile(std::string path, util::Buffer<char>&& content);
Dir* createDir(std::string path);
Dir* getDir(std::string_view path);
};
}

View file

@ -1,3 +1,5 @@
#pragma once
#include "Device.hpp"
namespace io {

View file

@ -239,7 +239,7 @@ std::unique_ptr<std::istream> ZipFileDevice::read(std::string_view path) {
size_t ZipFileDevice::size(std::string_view path) {
const auto& found = entries.find(std::string(path));
if (found == entries.end()) {
return false;
return 0;
}
return found->second.uncompressedSize;
}

View file

@ -0,0 +1,47 @@
#pragma once
#include <memory>
#include <ostream>
#include <functional>
class finalizing_ostream final : public std::ostream {
public:
finalizing_ostream(
std::unique_ptr<std::ostream> inner,
std::function<void(std::unique_ptr<std::ostream>)> on_destruction
)
: std::ostream(inner->rdbuf()),
innerStream(std::move(inner)),
onDestruction(on_destruction) {
}
finalizing_ostream(const finalizing_ostream&) = delete;
finalizing_ostream& operator=(const finalizing_ostream&) = delete;
finalizing_ostream(finalizing_ostream&& other) noexcept
: std::ostream(std::move(other)),
innerStream(std::move(other.innerStream)),
onDestruction(std::move(other.onDestruction)) {
other.onDestruction = nullptr;
}
finalizing_ostream& operator=(finalizing_ostream&& other) noexcept {
if (this != &other) {
std::ostream::operator=(std::move(other));
innerStream = std::move(other.innerStream);
onDestruction = std::move(other.onDestruction);
other.onDestruction = nullptr;
}
return *this;
}
~finalizing_ostream() {
if (onDestruction) {
onDestruction(std::move(innerStream));
}
}
private:
std::unique_ptr<std::ostream> innerStream;
std::function<void(std::unique_ptr<std::ostream>)> onDestruction;
};

View file

@ -32,3 +32,33 @@ public:
private:
memory_streambuf buf;
};
class memory_view_streambuf : public std::streambuf {
public:
explicit memory_view_streambuf(const util::Buffer<char>& buffer)
: buffer(std::move(buffer)) {
char* base = const_cast<char*>(this->buffer.data());
char* end = base + this->buffer.size();
setg(base, base, end);
}
memory_view_streambuf(const memory_view_streambuf&) = delete;
memory_view_streambuf& operator=(const memory_view_streambuf&) = delete;
protected:
int_type underflow() override {
return traits_type::eof();
}
private:
const util::Buffer<char>& buffer;
};
class memory_view_istream : public std::istream {
public:
explicit memory_view_istream(const util::Buffer<char>& buffer)
: std::istream(&buf), buf(buffer) {}
private:
memory_view_streambuf buf;
};

View file

@ -1,3 +1,5 @@
#pragma once
#include <iostream>
#include <streambuf>
#include <cstring>

View file

@ -51,6 +51,7 @@ SettingsHandler::SettingsHandler(EngineSettings& settings) {
builder.add("framerate", &settings.display.framerate);
builder.add("limit-fps-iconified", &settings.display.limitFpsIconified);
builder.add("window-mode", &settings.display.windowMode);
builder.add("adaptive-menu-fps", &settings.display.adaptiveFpsInMenu);
builder.section("camera");
builder.add("sensitivity", &settings.camera.sensitivity);
@ -78,6 +79,7 @@ SettingsHandler::SettingsHandler(EngineSettings& settings) {
builder.add("ssao", &settings.graphics.ssao);
builder.add("shadows-quality", &settings.graphics.shadowsQuality);
builder.add("dense-render-distance", &settings.graphics.denseRenderDistance);
builder.add("soft-lighting", &settings.graphics.softLighting);
builder.section("ui");
builder.add("language", &settings.ui.language);

View file

@ -15,6 +15,7 @@
#include "voxels/Chunks.hpp"
#include "voxels/GlobalChunks.hpp"
#include "world/Level.hpp"
#include "world/LevelEvents.hpp"
#include "world/World.hpp"
#include "world/generator/WorldGenerator.hpp"
@ -172,13 +173,12 @@ void ChunksController::createChunk(const Player& player, int x, int z) const {
auto chunk = level.chunks->create(x, z);
player.chunks->putChunk(chunk);
auto& chunkFlags = chunk->flags;
if (!chunkFlags.loaded) {
generator->generate(chunk->voxels, x, z);
chunkFlags.unsaved = true;
}
chunk->updateHeights();
level.events->trigger(LevelEventType::CHUNK_PRESENT, chunk.get());
if (!chunkFlags.loadedLights) {
Lighting::prebuildSkyLight(*chunk, *level.content.getIndices());
}

View file

@ -4,28 +4,29 @@
#include <algorithm>
#include <memory>
#include "engine/Engine.hpp"
#include "coders/commons.hpp"
#include "debug/Logger.hpp"
#include "coders/json.hpp"
#include "content/ContentReport.hpp"
#include "content/ContentControl.hpp"
#include "content/ContentReport.hpp"
#include "content/PacksManager.hpp"
#include "world/files/WorldConverter.hpp"
#include "world/files/WorldFiles.hpp"
#include "debug/Logger.hpp"
#include "engine/Engine.hpp"
#include "engine/EnginePaths.hpp"
#include "frontend/locale.hpp"
#include "frontend/menu.hpp"
#include "frontend/screens/LevelScreen.hpp"
#include "frontend/screens/MenuScreen.hpp"
#include "graphics/ui/GUI.hpp"
#include "graphics/ui/elements/Menu.hpp"
#include "graphics/ui/gui_util.hpp"
#include "objects/Players.hpp"
#include "graphics/ui/GUI.hpp"
#include "interfaces/Task.hpp"
#include "LevelController.hpp"
#include "objects/Players.hpp"
#include "util/stringutil.hpp"
#include "world/files/WorldConverter.hpp"
#include "world/files/WorldFiles.hpp"
#include "world/Level.hpp"
#include "world/World.hpp"
#include "LevelController.hpp"
static debug::Logger logger("engine-control");
@ -365,7 +366,9 @@ void EngineController::reconfigPacks(
);
}
} else {
auto world = controller->getLevel()->getWorld();
auto level = controller->getLevel();
auto world = level->getWorld();
controller->processBeforeQuit();
controller->saveWorld();
auto names = PacksManager::getNames(world->getPacks());

View file

@ -27,7 +27,8 @@ LevelController::LevelController(
: settings(engine->getSettings()),
level(std::move(levelPtr)),
chunks(std::make_unique<ChunksController>(*level)),
playerTickClock(20, 3) {
playerTickClock(20, 3),
localPlayer(clientPlayer) {
level->events->listen(LevelEventType::CHUNK_PRESENT, [](auto, Chunk* chunk) {
scripting::on_chunk_present(*chunk, chunk->flags.loaded);
@ -121,6 +122,16 @@ void LevelController::update(float delta, bool pause) {
level->entities->clean();
}
void LevelController::processBeforeQuit() {
// todo: move somewhere else
for (auto player : level->players->getAll()) {
if (player->chunks) {
player->chunks->saveAndClear();
}
}
scripting::process_before_quit();
}
void LevelController::saveWorld() {
auto world = level->getWorld();
if (world->isNameless()) {

View file

@ -20,6 +20,7 @@ class LevelController {
std::unique_ptr<ChunksController> chunks;
util::Clock playerTickClock;
Player* localPlayer;
public:
LevelController(Engine* engine, std::unique_ptr<Level> level, Player* clientPlayer);
@ -27,6 +28,7 @@ public:
/// @param pause is world and player simulation paused
void update(float delta, bool pause);
void processBeforeQuit();
void saveWorld();
void onWorldQuit();

View file

@ -207,8 +207,7 @@ void CameraControl::update(
tpCamera->front = camera->front;
tpCamera->right = camera->right;
}
if (player.currentCamera == spCamera || player.currentCamera == tpCamera ||
player.currentCamera == camera) {
if (player.isCurrentCameraBuiltin()) {
player.currentCamera->setFov(glm::radians(settings.fov.get()));
}
}
@ -280,7 +279,7 @@ void PlayerController::postUpdate(
updateFootsteps(delta);
}
if (!pause && input) {
if (!pause && input && player.isCurrentCameraBuiltin()) {
camControl.updateMouse(this->input, windowHeight);
}
camControl.refreshRotation();

View file

@ -1,6 +1,93 @@
#include "api_lua.hpp"
#include "io/io.hpp"
#include "io/devices/MemoryDevice.hpp"
#include "logic/scripting/scripting.hpp"
#include "content/ContentControl.hpp"
#include "engine/Engine.hpp"
#include "engine/EnginePaths.hpp"
#include "network/Network.hpp"
#include "util/platform.hpp"
#include "window/Window.hpp"
using namespace scripting;
static int l_start_debug_instance(lua::State* L) {
int port = lua::tointeger(L, 1);
if (port == 0) {
port = engine->getNetwork().findFreePort();
if (port == -1) {
throw std::runtime_error("could not find free port");
}
}
const auto& paths = engine->getPaths();
std::vector<std::string> args {
"--res", paths.getResourcesFolder().u8string(),
"--dir", paths.getUserFilesFolder().u8string(),
"--dbg-server", "tcp:" + std::to_string(port),
};
platform::new_engine_instance(std::move(args));
return lua::pushinteger(L, port);
}
static int l_focus(lua::State* L) {
engine->getWindow().focus();
return 0;
}
static int l_create_memory_device(lua::State* L) {
std::string name = lua::require_string(L, 1);
if (io::get_device(name)) {
throw std::runtime_error(
"entry-point '" + name + "' is already used"
);
}
if (name.find(':') != std::string::npos) {
throw std::runtime_error("invalid entry point name");
}
io::set_device(name, std::make_unique<io::MemoryDevice>());
return 0;
}
static int l_get_content_sources(lua::State* L) {
const auto& sources = engine->getContentControl().getContentSources();
lua::createtable(L, static_cast<int>(sources.size()), 0);
for (size_t i = 0; i < sources.size(); i++) {
lua::pushlstring(L, sources[i].string());
lua::rawseti(L, static_cast<int>(i + 1));
}
return 1;
}
static int l_set_content_sources(lua::State* L) {
if (!lua::istable(L, 1)) {
throw std::runtime_error("table expected as argument 1");
}
int len = lua::objlen(L, 1);
std::vector<io::path> sources;
for (int i = 0; i < len; i++) {
lua::rawgeti(L, i + 1);
sources.emplace_back(std::string(lua::require_lstring(L, -1)));
lua::pop(L);
}
engine->getContentControl().setContentSources(std::move(sources));
return 0;
}
static int l_reset_content_sources(lua::State* L) {
engine->getContentControl().resetContentSources();
return 0;
}
const luaL_Reg applib[] = {
// see libcore.cpp an stdlib.lua
{"start_debug_instance", lua::wrap<l_start_debug_instance>},
{"focus", lua::wrap<l_focus>},
{"create_memory_device", lua::wrap<l_create_memory_device>},
{"get_content_sources", lua::wrap<l_get_content_sources>},
{"set_content_sources", lua::wrap<l_set_content_sources>},
{"reset_content_sources", lua::wrap<l_reset_content_sources>},
// for other functions see libcore.cpp and stdlib.lua
{nullptr, nullptr}
};

View file

@ -7,7 +7,9 @@
#include "engine/Engine.hpp"
#include "graphics/commons/Model.hpp"
#include "graphics/core/Texture.hpp"
#include "graphics/core/Atlas.hpp"
#include "util/Buffer.hpp"
#include "../usertypes/lua_type_canvas.hpp"
using namespace scripting;
@ -64,8 +66,47 @@ static int l_parse_model(lua::State* L) {
return 0;
}
static int l_to_canvas(lua::State* L) {
auto& assets = *engine->getAssets();
auto alias = lua::require_lstring(L, 1);
size_t sep = alias.rfind(':');
if (sep == std::string::npos) {
auto texture = assets.getShared<Texture>(std::string(alias));
if (texture != nullptr) {
auto image = texture->readData();
return lua::newuserdata<lua::LuaCanvas>(
L, texture, std::move(image)
);
}
return 0;
}
auto atlasName = alias.substr(0, sep);
if (auto atlas = assets.get<Atlas>(std::string(atlasName))) {
auto textureName = std::string(alias.substr(sep + 1));
auto image = atlas->shareImageData();
auto texture = atlas->shareTexture();
if (auto region = atlas->getIf(textureName)) {
UVRegion uvRegion = *region;
int atlasWidth = static_cast<int>(image->getWidth());
int atlasHeight = static_cast<int>(image->getHeight());
int x = static_cast<int>(uvRegion.u1 * atlasWidth);
int y = static_cast<int>(uvRegion.v1 * atlasHeight);
int w = static_cast<int>(uvRegion.getWidth() * atlasWidth);
int h = static_cast<int>(uvRegion.getHeight() * atlasHeight);
return lua::newuserdata<lua::LuaCanvas>(
L, std::move(texture), image->cropped(x, y, w, h), uvRegion
);
}
}
return 0;
}
const luaL_Reg assetslib[] = {
{"load_texture", lua::wrap<l_load_texture>},
{"parse_model", lua::wrap<l_parse_model>},
{"to_canvas", lua::wrap<l_to_canvas>},
{nullptr, nullptr}
};

View file

@ -1,6 +1,7 @@
#include "audio/audio.hpp"
#include "assets/Assets.hpp"
#include "engine/Engine.hpp"
#include "engine/EnginePaths.hpp"
#include "api_lua.hpp"
inline const char* DEFAULT_CHANNEL = "regular";
@ -67,6 +68,26 @@ inline audio::speakerid_t play_stream(
if (channel == -1) {
return 0;
}
if (!scripting::engine->isHeadless()) {
auto assets = scripting::engine->getAssets();
auto stream = assets->getShared<audio::PCMStream>(filename);
if (stream) {
return audio::play(
audio::open_stream(std::move(stream), true),
glm::vec3(
static_cast<float>(x),
static_cast<float>(y),
static_cast<float>(z)
),
relative,
volume,
pitch,
loop,
channel
);
}
}
io::path file;
if (std::strchr(filename, ':')) {
file = std::string(filename);
@ -359,16 +380,80 @@ static int l_audio_get_velocity(lua::State* L) {
return 0;
}
// @brief audio.count_speakers() -> integer
/// @brief audio.count_speakers() -> integer
static int l_audio_count_speakers(lua::State* L) {
return lua::pushinteger(L, audio::count_speakers());
}
// @brief audio.count_streams() -> integer
/// @brief audio.count_streams() -> integer
static int l_audio_count_streams(lua::State* L) {
return lua::pushinteger(L, audio::count_streams());
}
/// @brief audio.input.fetch(size) -> Bytearray
static int l_audio_fetch_input(lua::State* L) {
auto device = audio::get_input_device();
if (device == nullptr) {
return 0;
}
size_t size = lua::touinteger(L, 1);
const size_t MAX_BUFFER_SIZE = audio::MAX_INPUT_SAMPLES * 4;
if (size == 0) {
size = MAX_BUFFER_SIZE;
}
size = std::min<size_t>(size, MAX_BUFFER_SIZE);
ubyte buffer[MAX_BUFFER_SIZE];
size = device->read(reinterpret_cast<char*>(buffer), size);
std::vector<ubyte> bytes(buffer, buffer + size);
return lua::create_bytearray(L, std::move(bytes));
}
static int l_audio_get_input_devices_names(lua::State* L) {
auto device_names = audio::get_input_devices_names();
lua::createtable(L, device_names.size(), 0);
int index = 1;
for (const auto& name : device_names) {
lua::pushstring(L, name.c_str());
lua::rawseti(L, index++);
}
return 1;
}
static int l_audio_get_output_devices_names(lua::State* L) {
auto device_names = audio::get_output_devices_names();
lua::createtable(L, device_names.size(), 0);
int index = 1;
for (const auto& name : device_names) {
lua::pushstring(L, name.c_str());
lua::rawseti(L, index++);
}
return 1;
}
static int l_audio_set_input_device(lua::State* L) {
auto device_name = lua::tostring(L, 1);
audio::set_input_device(device_name);
return 0;
}
static int l_audio_get_input_info(lua::State* L) {
auto device = audio::get_input_device();
if (device == nullptr) {
return 0;
}
lua::createtable(L, 0, 4);
lua::pushlstring(L, device->getDeviceSpecifier());
lua::setfield(L, "device_specifier");
lua::pushinteger(L, device->getChannels());
lua::setfield(L, "channels");
lua::pushinteger(L, device->getSampleRate());
lua::setfield(L, "sample_rate");
lua::pushinteger(L, device->getBitsPerSample());
lua::setfield(L, "bits_per_sample");
return 1;
}
const luaL_Reg audiolib[] = {
{"play_sound", lua::wrap<l_audio_play_sound>},
{"play_sound_2d", lua::wrap<l_audio_play_sound_2d>},
@ -394,5 +479,10 @@ const luaL_Reg audiolib[] = {
{"get_velocity", lua::wrap<l_audio_get_velocity>},
{"count_speakers", lua::wrap<l_audio_count_speakers>},
{"count_streams", lua::wrap<l_audio_count_streams>},
{"__fetch_input", lua::wrap<l_audio_fetch_input>},
{"get_input_devices_names", lua::wrap<l_audio_get_input_devices_names>},
{"get_output_devices_names", lua::wrap<l_audio_get_output_devices_names>},
{"set_input_device", lua::wrap<l_audio_set_input_device>},
{"get_input_info", lua::wrap<l_audio_get_input_info>},
{nullptr, nullptr}
};

View file

@ -1,7 +1,6 @@
#include "coders/binary_json.hpp"
#include "api_lua.hpp"
#include "util/Buffer.hpp"
#include "../lua_custom_types.hpp"
static int l_tobytes(lua::State* L) {
auto value = lua::tovalue(L, 1);

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