summaryrefslogtreecommitdiffstats
path: root/core
diff options
context:
space:
mode:
authoruntodesu <kirill@untode.su>2025-03-15 16:22:09 +0500
committeruntodesu <kirill@untode.su>2025-03-15 16:22:09 +0500
commit3bf42c6ff3805a0d42bbc661794a95ff31bedc26 (patch)
tree05049955847504808d6bed2bb7b155f8b03807bb /core
parent02294547dcde0d4ad76e229106702261e9f10a51 (diff)
downloadvoxelius-3bf42c6ff3805a0d42bbc661794a95ff31bedc26.tar.bz2
voxelius-3bf42c6ff3805a0d42bbc661794a95ff31bedc26.zip
Add whatever I was working on for the last month
Diffstat (limited to 'core')
-rw-r--r--core/CMakeLists.txt43
-rw-r--r--core/aabb.cc62
-rw-r--r--core/aabb.hh31
-rw-r--r--core/angles.hh105
-rw-r--r--core/binfile.cc67
-rw-r--r--core/binfile.hh10
-rw-r--r--core/buffer.cc203
-rw-r--r--core/buffer.hh254
-rw-r--r--core/cmdline.cc72
-rw-r--r--core/cmdline.hh13
-rw-r--r--core/config.cc179
-rw-r--r--core/config.hh181
-rw-r--r--core/constexpr.hh179
-rw-r--r--core/crc64.cc93
-rw-r--r--core/crc64.hh12
-rw-r--r--core/epoch.cc38
-rw-r--r--core/epoch.hh19
-rw-r--r--core/feature.hh.in5
-rw-r--r--core/floathacks.hh50
-rw-r--r--core/image.cc104
-rw-r--r--core/image.hh13
-rw-r--r--core/macros.hh7
-rw-r--r--core/pch.hh49
-rw-r--r--core/randomizer.hh57
-rw-r--r--core/resource.hh18
-rw-r--r--core/strtools.cc47
-rw-r--r--core/strtools.hh21
-rw-r--r--core/version.hh.in12
28 files changed, 1944 insertions, 0 deletions
diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt
new file mode 100644
index 0000000..a8a5468
--- /dev/null
+++ b/core/CMakeLists.txt
@@ -0,0 +1,43 @@
+add_library(core STATIC
+ "${CMAKE_CURRENT_LIST_DIR}/aabb.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/aabb.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/binfile.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/binfile.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/buffer.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/buffer.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/cmdline.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/cmdline.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/config.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/config.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/constexpr.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/crc64.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/crc64.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/epoch.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/epoch.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/feature.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/floathacks.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/image.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/image.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/pch.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/resource.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/strtools.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/strtools.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/version.hh")
+target_compile_features(core PUBLIC cxx_std_17)
+target_include_directories(core PUBLIC "${DEPS_INCLUDE_DIR}")
+target_include_directories(core PUBLIC "${PROJECT_SOURCE_DIR}")
+target_precompile_headers(core PRIVATE "${CMAKE_CURRENT_LIST_DIR}/pch.hh")
+target_link_libraries(core PUBLIC enet physfs spdlog stb)
+
+if(WIN32)
+ target_compile_definitions(core PUBLIC _CRT_SECURE_NO_WARNINGS)
+ target_compile_definitions(core PUBLIC _USE_MATH_DEFINES)
+ target_compile_definitions(core PUBLIC NOMINMAX)
+endif()
+
+if(MSVC)
+ target_compile_options(core PUBLIC /utf-8)
+endif()
+
+configure_file("${CMAKE_CURRENT_LIST_DIR}/feature.hh.in" "${CMAKE_CURRENT_LIST_DIR}/feature.hh")
+configure_file("${CMAKE_CURRENT_LIST_DIR}/version.hh.in" "${CMAKE_CURRENT_LIST_DIR}/version.hh")
diff --git a/core/aabb.cc b/core/aabb.cc
new file mode 100644
index 0000000..48070fe
--- /dev/null
+++ b/core/aabb.cc
@@ -0,0 +1,62 @@
+#include "core/pch.hh"
+#include "core/aabb.hh"
+
+AABB::AABB(const glm::fvec3 &min, const glm::fvec3 &max)
+{
+ set_bounds(min, max);
+}
+
+void AABB::set_bounds(const glm::fvec3 &min, const glm::fvec3 &max)
+{
+ this->min = min;
+ this->max = max;
+}
+
+void AABB::set_offset(const glm::fvec3 &base, const glm::fvec3 &size)
+{
+ this->min = base;
+ this->max = base + size;
+}
+
+bool AABB::contains(const glm::fvec3 &point) const
+{
+ if((point.x < min.x) || (point.x > max.x))
+ return false;
+ if((point.y < min.y) || (point.y > max.y))
+ return false;
+ if((point.z < min.z) || (point.z > max.z))
+ return false;
+ return true;
+}
+
+bool AABB::intersect(const AABB &other_box) const
+{
+ if((min.x >= other_box.max.x) || (max.x <= other_box.min.x))
+ return false;
+ if((min.y >= other_box.max.y) || (max.y <= other_box.min.y))
+ return false;
+ if((min.z >= other_box.max.z) || (max.z <= other_box.min.z))
+ return false;
+ return true;
+}
+
+AABB AABB::combine_with(const AABB &other_box) const
+{
+ AABB result;
+ result.set_bounds(min, other_box.max);
+ return result;
+}
+
+AABB AABB::multiply_with(const AABB &other_box) const
+{
+ AABB result;
+ result.set_bounds(other_box.min, max);
+ return result;
+}
+
+AABB AABB::push(const glm::fvec3 &vector) const
+{
+ AABB result;
+ result.set_bounds(min + vector, max + vector);
+ return result;
+}
diff --git a/core/aabb.hh b/core/aabb.hh
new file mode 100644
index 0000000..61193a4
--- /dev/null
+++ b/core/aabb.hh
@@ -0,0 +1,31 @@
+#ifndef CORE_AABB_HH
+#define CORE_AABB_HH 1
+#pragma once
+
+#include "core/macros.hh"
+
+class AABB final {
+public:
+ DECLARE_DEFAULT_CTOR(AABB);
+ explicit AABB(const glm::fvec3 &min, const glm::fvec3 &max);
+ virtual ~AABB(void) = default;
+
+ void set_bounds(const glm::fvec3 &min, const glm::fvec3 &max);
+ void set_offset(const glm::fvec3 &base, const glm::fvec3 &size);
+
+ const glm::fvec3 &get_min(void) const;
+ const glm::fvec3 &get_max(void) const;
+
+ bool contains(const glm::fvec3 &point) const;
+ bool intersect(const AABB &other_box) const;
+
+ AABB combine_with(const AABB &other_box) const;
+ AABB multiply_with(const AABB &other_box) const;
+ AABB push(const glm::fvec3 &vector) const;
+
+public:
+ glm::fvec3 min;
+ glm::fvec3 max;
+};
+
+#endif /* CORE_AABB_HH */
diff --git a/core/angles.hh b/core/angles.hh
new file mode 100644
index 0000000..0d8e41d
--- /dev/null
+++ b/core/angles.hh
@@ -0,0 +1,105 @@
+#ifndef CORE_ANGLES_HH
+#define CORE_ANGLES_HH 1
+#pragma once
+
+#include "core/constexpr.hh"
+
+constexpr float A180 = cxpr::radians(180.0f);
+constexpr float A360 = cxpr::radians(360.0f);
+
+namespace cxangles
+{
+float wrap_180(float angle);
+float wrap_360(float angle);
+} // namespace cxangles
+
+namespace cxangles
+{
+glm::fvec3 wrap_180(const glm::fvec3 &angles);
+glm::fvec3 wrap_360(const glm::fvec3 &angles);
+} // namespace cxangles
+
+namespace cxangles
+{
+void vectors(const glm::fvec3 &angles, glm::fvec3 &forward);
+void vectors(const glm::fvec3 &angles, glm::fvec3 *forward, glm::fvec3 *right, glm::fvec3 *up);
+} // namespace cxangles
+
+inline float cxangles::wrap_180(float angle)
+{
+ const auto result = std::fmod(angle + A180, A360);
+
+ if(result < 0.0f)
+ return result + A180;
+ return result - A180;
+}
+
+inline float cxangles::wrap_360(float angle)
+{
+ return std::fmod(std::fmod(angle, A360) + A360, A360);
+}
+
+inline glm::fvec3 cxangles::wrap_180(const glm::fvec3 &angles)
+{
+ return glm::fvec3 {
+ cxangles::wrap_180(angles.x),
+ cxangles::wrap_180(angles.y),
+ cxangles::wrap_180(angles.z),
+ };
+}
+
+inline glm::fvec3 cxangles::wrap_360(const glm::fvec3 &angles)
+{
+ return glm::fvec3 {
+ cxangles::wrap_360(angles.x),
+ cxangles::wrap_360(angles.y),
+ cxangles::wrap_360(angles.z),
+ };
+}
+
+inline void cxangles::vectors(const glm::fvec3 &angles, glm::fvec3 &forward)
+{
+ const float cosp = std::cos(angles.x);
+ const float cosy = std::cos(angles.y);
+ const float sinp = std::sin(angles.x);
+ const float siny = std::sin(angles.y);
+
+ forward.x = cosp * siny * (-1.0f);
+ forward.y = sinp;
+ forward.z = cosp * cosy * (-1.0f);
+}
+
+inline void cxangles::vectors(const glm::fvec3 &angles, glm::fvec3 *forward, glm::fvec3 *right, glm::fvec3 *up)
+{
+ if(!forward && !right && !up) {
+ // There's no point in figuring out
+ // direction vectors if nothing is passed
+ // in the function to store that stuff in
+ return;
+ }
+
+ const auto pcv = glm::cos(angles);
+ const auto psv = glm::sin(angles);
+ const auto ncv = pcv * (-1.0f);
+ const auto nsv = psv * (-1.0f);
+
+ if(forward) {
+ forward->x = pcv.x * nsv.y;
+ forward->y = psv.x;
+ forward->z = pcv.x * ncv.y;
+ }
+
+ if(right) {
+ right->x = pcv.z * pcv.y;
+ right->y = psv.z * pcv.y;
+ right->z = nsv.y;
+ }
+
+ if(up) {
+ up->x = psv.x * psv.y * pcv.z + ncv.y * psv.z;
+ up->y = pcv.x * pcv.z;
+ up->z = nsv.x * ncv.y * pcv.z + psv.y * psv.z;
+ }
+}
+
+#endif /* CORE_ANGLES_HH */
diff --git a/core/binfile.cc b/core/binfile.cc
new file mode 100644
index 0000000..34af3ca
--- /dev/null
+++ b/core/binfile.cc
@@ -0,0 +1,67 @@
+#include "core/pch.hh"
+#include "core/binfile.hh"
+
+#include "core/resource.hh"
+
+static emhash8::HashMap<std::string, resource_ptr<BinFile>> resource_map;
+
+template<>
+resource_ptr<BinFile> resource::load<BinFile>(const char *name, unsigned int flags)
+{
+ auto it = resource_map.find(name);
+
+ if(it != resource_map.cend()) {
+ // Return an existing resource
+ return it->second;
+ }
+
+ auto file = PHYSFS_openRead(name);
+
+ if(file == nullptr) {
+ spdlog::warn("resource: {}: {}", name, PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode()));
+ return nullptr;
+ }
+
+ auto new_resource = std::make_shared<BinFile>();
+ new_resource->size = PHYSFS_fileLength(file);
+ new_resource->buffer = new std::byte[new_resource->size];
+
+ PHYSFS_readBytes(file, new_resource->buffer, new_resource->size);
+ PHYSFS_close(file);
+
+ return resource_map.insert_or_assign(name, new_resource).first->second;
+}
+
+template<>
+void resource::hard_cleanup<BinFile>(void)
+{
+ for(const auto &it : resource_map) {
+ if(it.second.use_count() > 1L)
+ spdlog::warn("resource: zombie resource [BinFile] {} [use_count={}]", it.first, it.second.use_count());
+ else spdlog::debug("resource: releasing [BinFile] {}", it.first);
+
+ delete[] it.second->buffer;
+ }
+
+ resource_map.clear();
+}
+
+template<>
+void resource::soft_cleanup<BinFile>(void)
+{
+ auto iter = resource_map.cbegin();
+
+ while(iter != resource_map.cend()) {
+ if(iter->second.use_count() == 1L) {
+ spdlog::debug("resource: releasing [BinFile] {}", iter->first);
+
+ delete[] iter->second->buffer;
+
+ iter = resource_map.erase(iter);
+
+ continue;
+ }
+
+ iter = std::next(iter);
+ }
+}
diff --git a/core/binfile.hh b/core/binfile.hh
new file mode 100644
index 0000000..1e2e6c3
--- /dev/null
+++ b/core/binfile.hh
@@ -0,0 +1,10 @@
+#ifndef CORE_BINFILE_HH
+#define CORE_BINFILE_HH 1
+#pragma once
+
+struct BinFile final {
+ std::byte *buffer;
+ std::size_t size;
+};
+
+#endif /* CORE_BINFILE_HH */
diff --git a/core/buffer.cc b/core/buffer.cc
new file mode 100644
index 0000000..2d37c84
--- /dev/null
+++ b/core/buffer.cc
@@ -0,0 +1,203 @@
+#include "core/pch.hh"
+#include "core/buffer.hh"
+
+#include "core/constexpr.hh"
+
+ReadBuffer::ReadBuffer(const void *data, std::size_t size)
+{
+ reset(data, size);
+}
+
+ReadBuffer::ReadBuffer(const ENetPacket *packet)
+{
+ reset(packet);
+}
+
+ReadBuffer::ReadBuffer(PHYSFS_File *file)
+{
+ reset(file);
+}
+
+std::size_t ReadBuffer::size(void) const
+{
+ return m_vector.size();
+}
+
+const std::byte *ReadBuffer::data(void) const
+{
+ return m_vector.data();
+}
+
+void ReadBuffer::reset(const void *data, std::size_t size)
+{
+ auto bytes = reinterpret_cast<const std::byte *>(data);
+ m_vector.assign(bytes, bytes + size);
+ m_position = 0U;
+}
+
+void ReadBuffer::reset(const ENetPacket *packet)
+{
+ auto bytes_ptr = reinterpret_cast<const std::byte *>(packet->data);
+ m_vector.assign(bytes_ptr, bytes_ptr + packet->dataLength);
+ m_position = 0;
+}
+
+void ReadBuffer::reset(PHYSFS_File *file)
+{
+ m_vector.resize(PHYSFS_fileLength(file));
+ m_position = 0;
+
+ PHYSFS_seek(file, 0);
+ PHYSFS_readBytes(file, m_vector.data(), m_vector.size());
+}
+
+float ReadBuffer::read_FP32(void)
+{
+ return floathacks::uint32_to_float(read_UI32());
+}
+
+std::uint8_t ReadBuffer::read_UI8(void)
+{
+ if((m_position + 1U) <= m_vector.size()) {
+ auto result = static_cast<std::uint8_t>(m_vector[m_position]);
+ m_position += 1U;
+ return result;
+ }
+
+ m_position += 1U;
+ return 0;
+}
+
+std::uint16_t ReadBuffer::read_UI16(void)
+{
+ if((m_position + 2U) <= m_vector.size()) {
+ auto result = UINT16_C(0x0000);
+ result |= static_cast<std::uint16_t>(m_vector[m_position + 0U]) << 8U;
+ result |= static_cast<std::uint16_t>(m_vector[m_position + 1U]) << 0U;
+ m_position += 2U;
+ return result;
+ }
+
+ m_position += 2U;
+ return 0;
+}
+
+std::uint32_t ReadBuffer::read_UI32(void)
+{
+ if((m_position + 4U) <= m_vector.size()) {
+ auto result = UINT32_C(0x00000000);
+ result |= static_cast<std::uint32_t>(m_vector[m_position + 0U]) << 24U;
+ result |= static_cast<std::uint32_t>(m_vector[m_position + 1U]) << 16U;
+ result |= static_cast<std::uint32_t>(m_vector[m_position + 2U]) << 8U;
+ result |= static_cast<std::uint32_t>(m_vector[m_position + 3U]) << 0U;
+ m_position += 4U;
+ return result;
+ }
+
+ m_position += 4U;
+ return 0;
+}
+
+std::uint64_t ReadBuffer::read_UI64(void)
+{
+ if((m_position + 8U) <= m_vector.size()) {
+ auto result = UINT64_C(0x0000000000000000);
+ result |= (0x00000000000000FF & static_cast<std::uint64_t>(m_vector[m_position + 0U])) << 56U;
+ result |= (0x00000000000000FF & static_cast<std::uint64_t>(m_vector[m_position + 1U])) << 48U;
+ result |= (0x00000000000000FF & static_cast<std::uint64_t>(m_vector[m_position + 2U])) << 40U;
+ result |= (0x00000000000000FF & static_cast<std::uint64_t>(m_vector[m_position + 3U])) << 32U;
+ result |= (0x00000000000000FF & static_cast<std::uint64_t>(m_vector[m_position + 4U])) << 24U;
+ result |= (0x00000000000000FF & static_cast<std::uint64_t>(m_vector[m_position + 5U])) << 16U;
+ result |= (0x00000000000000FF & static_cast<std::uint64_t>(m_vector[m_position + 6U])) << 8U;
+ result |= (0x00000000000000FF & static_cast<std::uint64_t>(m_vector[m_position + 7U])) << 0U;
+ m_position += 8U;
+ return result;
+ }
+
+ m_position += 8U;
+ return 0;
+}
+
+std::string ReadBuffer::read_string(void)
+{
+ auto size = static_cast<std::size_t>(read_UI16());
+ auto result = std::string();
+
+ for(std::size_t i = 0; i < size; ++i) {
+ if(m_position < m_vector.size())
+ result.push_back(static_cast<char>(m_vector[m_position]));
+ m_position += 1U;
+ }
+
+ return result;
+}
+
+std::size_t WriteBuffer::size(void) const
+{
+ return m_vector.size();
+}
+
+const std::byte *WriteBuffer::data(void) const
+{
+ return m_vector.data();
+}
+
+void WriteBuffer::reset(void)
+{
+ m_vector.clear();
+}
+
+void WriteBuffer::write_UI8(std::uint8_t value)
+{
+ m_vector.push_back(static_cast<std::byte>(value));
+}
+
+void WriteBuffer::write_UI16(std::uint16_t value)
+{
+ m_vector.push_back(static_cast<std::byte>(UINT16_C(0xFF) & ((value & UINT16_C(0xFF00)) >> 8U)));
+ m_vector.push_back(static_cast<std::byte>(UINT16_C(0xFF) & ((value & UINT16_C(0x00FF)) >> 0U)));
+}
+
+void WriteBuffer::write_UI32(std::uint32_t value)
+{
+ m_vector.push_back(static_cast<std::byte>(UINT32_C(0xFF) & ((value & UINT32_C(0xFF000000)) >> 24U)));
+ m_vector.push_back(static_cast<std::byte>(UINT32_C(0xFF) & ((value & UINT32_C(0x00FF0000)) >> 16U)));
+ m_vector.push_back(static_cast<std::byte>(UINT32_C(0xFF) & ((value & UINT32_C(0x0000FF00)) >> 8U)));
+ m_vector.push_back(static_cast<std::byte>(UINT32_C(0xFF) & ((value & UINT32_C(0x000000FF)) >> 0U)));
+}
+
+void WriteBuffer::write_UI64(std::uint64_t value)
+{
+ m_vector.push_back(static_cast<std::byte>(UINT64_C(0xFF) & ((value & UINT64_C(0xFF00000000000000)) >> 56U)));
+ m_vector.push_back(static_cast<std::byte>(UINT64_C(0xFF) & ((value & UINT64_C(0x00FF000000000000)) >> 48U)));
+ m_vector.push_back(static_cast<std::byte>(UINT64_C(0xFF) & ((value & UINT64_C(0x0000FF0000000000)) >> 40U)));
+ m_vector.push_back(static_cast<std::byte>(UINT64_C(0xFF) & ((value & UINT64_C(0x000000FF00000000)) >> 32U)));
+ m_vector.push_back(static_cast<std::byte>(UINT64_C(0xFF) & ((value & UINT64_C(0x00000000FF000000)) >> 24U)));
+ m_vector.push_back(static_cast<std::byte>(UINT64_C(0xFF) & ((value & UINT64_C(0x0000000000FF0000)) >> 16U)));
+ m_vector.push_back(static_cast<std::byte>(UINT64_C(0xFF) & ((value & UINT64_C(0x000000000000FF00)) >> 8U)));
+ m_vector.push_back(static_cast<std::byte>(UINT64_C(0xFF) & ((value & UINT64_C(0x00000000000000FF)) >> 0U)));
+}
+
+void WriteBuffer::write_string(const std::string &value)
+{
+ const std::size_t size = cxpr::min<std::size_t>(UINT16_MAX, value.size());
+
+ write_UI16(static_cast<std::uint16_t>(size));
+
+ for(std::size_t i = 0; i < size; m_vector.push_back(static_cast<std::byte>(value[i++])));
+}
+
+PHYSFS_File *WriteBuffer::to_file(const char *path, bool append) const
+{
+ if(auto file = (append ? PHYSFS_openAppend(path) : PHYSFS_openWrite(path))) {
+ PHYSFS_writeBytes(file, m_vector.data(), m_vector.size());
+ return file;
+ }
+
+ return nullptr;
+}
+
+ENetPacket *WriteBuffer::to_packet(enet_uint32 flags) const
+{
+ return enet_packet_create(m_vector.data(), m_vector.size(), flags);
+}
diff --git a/core/buffer.hh b/core/buffer.hh
new file mode 100644
index 0000000..5eb96e5
--- /dev/null
+++ b/core/buffer.hh
@@ -0,0 +1,254 @@
+#ifndef CORE_BUFFER_HH
+#define CORE_BUFFER_HH 1
+
+#include "core/floathacks.hh"
+
+class ReadBuffer final {
+public:
+ explicit ReadBuffer(void) = default;
+ explicit ReadBuffer(const void *data, std::size_t size);
+ explicit ReadBuffer(const ENetPacket *packet);
+ explicit ReadBuffer(PHYSFS_File *file);
+ virtual ~ReadBuffer(void) = default;
+
+ std::size_t size(void) const;
+ const std::byte *data(void) const;
+
+ void reset(const void *data, std::size_t size);
+ void reset(const ENetPacket *packet);
+ void reset(PHYSFS_File *file);
+
+ float read_FP32(void);
+ std::uint8_t read_UI8(void);
+ std::uint16_t read_UI16(void);
+ std::uint32_t read_UI32(void);
+ std::uint64_t read_UI64(void);
+ std::string read_string(void);
+
+ inline std::int8_t read_I8(void);
+ inline std::int16_t read_I16(void);
+ inline std::int32_t read_I32(void);
+ inline std::int64_t read_I64(void);
+
+ inline ReadBuffer &operator>>(float &value);
+ inline ReadBuffer &operator>>(std::int8_t &value);
+ inline ReadBuffer &operator>>(std::int16_t &value);
+ inline ReadBuffer &operator>>(std::int32_t &value);
+ inline ReadBuffer &operator>>(std::int64_t &value);
+ inline ReadBuffer &operator>>(std::uint8_t &value);
+ inline ReadBuffer &operator>>(std::uint16_t &value);
+ inline ReadBuffer &operator>>(std::uint32_t &value);
+ inline ReadBuffer &operator>>(std::uint64_t &value);
+ inline ReadBuffer &operator>>(std::string &value);
+
+private:
+ std::vector<std::byte> m_vector;
+ std::size_t m_position;
+};
+
+class WriteBuffer final {
+public:
+ explicit WriteBuffer(void) = default;
+ virtual ~WriteBuffer(void) = default;
+
+ std::size_t size(void) const;
+ const std::byte *data(void) const;
+
+ void reset(void);
+
+ void write_FP32(float value);
+ void write_UI8(std::uint8_t value);
+ void write_UI16(std::uint16_t value);
+ void write_UI32(std::uint32_t value);
+ void write_UI64(std::uint64_t value);
+ void write_string(const std::string &value);
+
+ inline void write_I8(std::int8_t value);
+ inline void write_I16(std::int16_t value);
+ inline void write_I32(std::int32_t value);
+ inline void write_I64(std::int64_t value);
+
+ inline WriteBuffer &operator<<(float value);
+ inline WriteBuffer &operator<<(std::int8_t value);
+ inline WriteBuffer &operator<<(std::int16_t value);
+ inline WriteBuffer &operator<<(std::int32_t value);
+ inline WriteBuffer &operator<<(std::int64_t value);
+ inline WriteBuffer &operator<<(std::uint8_t value);
+ inline WriteBuffer &operator<<(std::uint16_t value);
+ inline WriteBuffer &operator<<(std::uint32_t value);
+ inline WriteBuffer &operator<<(std::uint64_t value);
+ inline WriteBuffer &operator<<(const std::string &value);
+
+ PHYSFS_File *to_file(const char *path, bool append = false) const;
+ ENetPacket *to_packet(enet_uint32 flags = ENET_PACKET_FLAG_RELIABLE) const;
+
+private:
+ std::vector<std::byte> m_vector;
+};
+
+inline std::int8_t ReadBuffer::read_I8(void)
+{
+ return static_cast<std::int8_t>(read_UI8());
+}
+
+inline std::int16_t ReadBuffer::read_I16(void)
+{
+ return static_cast<std::int16_t>(read_UI16());
+}
+
+inline std::int32_t ReadBuffer::read_I32(void)
+{
+ return static_cast<std::int32_t>(read_UI32());
+}
+
+inline std::int64_t ReadBuffer::read_I64(void)
+{
+ return static_cast<std::int64_t>(read_UI64());
+}
+
+inline ReadBuffer &ReadBuffer::operator>>(float &value)
+{
+ value = read_FP32();
+ return *this;
+}
+
+inline ReadBuffer &ReadBuffer::operator>>(std::int8_t &value)
+{
+ value = read_I8();
+ return *this;
+}
+
+inline ReadBuffer &ReadBuffer::operator>>(std::int16_t &value)
+{
+ value = read_I16();
+ return *this;
+}
+
+inline ReadBuffer &ReadBuffer::operator>>(std::int32_t &value)
+{
+ value = read_I32();
+ return *this;
+}
+
+inline ReadBuffer &ReadBuffer::operator>>(std::int64_t &value)
+{
+ value = read_I64();
+ return *this;
+}
+
+inline ReadBuffer &ReadBuffer::operator>>(std::uint8_t &value)
+{
+ value = read_UI8();
+ return *this;
+}
+
+inline ReadBuffer &ReadBuffer::operator>>(std::uint16_t &value)
+{
+ value = read_UI16();
+ return *this;
+}
+
+inline ReadBuffer &ReadBuffer::operator>>(std::uint32_t &value)
+{
+ value = read_UI32();
+ return *this;
+}
+
+inline ReadBuffer &ReadBuffer::operator>>(std::uint64_t &value)
+{
+ value = read_UI64();
+ return *this;
+}
+
+inline ReadBuffer &ReadBuffer::operator>>(std::string &value)
+{
+ value = read_string();
+ return *this;
+}
+
+inline void WriteBuffer::write_FP32(float value)
+{
+ write_UI32(floathacks::float_to_uint32(value));
+}
+
+inline void WriteBuffer::write_I8(std::int8_t value)
+{
+ write_UI8(static_cast<std::uint8_t>(value));
+}
+
+inline void WriteBuffer::write_I16(std::int16_t value)
+{
+ write_UI16(static_cast<std::uint16_t>(value));
+}
+
+inline void WriteBuffer::write_I32(std::int32_t value)
+{
+ write_UI32(static_cast<std::uint32_t>(value));
+}
+
+inline void WriteBuffer::write_I64(std::int64_t value)
+{
+ write_UI64(static_cast<std::uint64_t>(value));
+}
+
+inline WriteBuffer &WriteBuffer::operator<<(float value)
+{
+ write_FP32(value);
+ return *this;
+}
+
+inline WriteBuffer &WriteBuffer::operator<<(std::int8_t value)
+{
+ write_I8(value);
+ return *this;
+}
+
+inline WriteBuffer &WriteBuffer::operator<<(std::int16_t value)
+{
+ write_I16(value);
+ return *this;
+}
+
+inline WriteBuffer &WriteBuffer::operator<<(std::int32_t value)
+{
+ write_I32(value);
+ return *this;
+}
+
+inline WriteBuffer &WriteBuffer::operator<<(std::int64_t value)
+{
+ write_I64(value);
+ return *this;
+}
+
+inline WriteBuffer &WriteBuffer::operator<<(std::uint8_t value)
+{
+ write_UI8(value);
+ return *this;
+}
+
+inline WriteBuffer &WriteBuffer::operator<<(std::uint16_t value)
+{
+ write_UI16(value);
+ return *this;
+}
+
+inline WriteBuffer &WriteBuffer::operator<<(std::uint32_t value)
+{
+ write_UI32(value);
+ return *this;
+}
+
+inline WriteBuffer &WriteBuffer::operator<<(std::uint64_t value)
+{
+ write_UI64(value);
+ return *this;
+}
+
+inline WriteBuffer &WriteBuffer::operator<<(const std::string &value)
+{
+ write_string(value);
+ return *this;
+}
+
+#endif /* CORE_BUFFER_HH */
diff --git a/core/cmdline.cc b/core/cmdline.cc
new file mode 100644
index 0000000..4cc7de0
--- /dev/null
+++ b/core/cmdline.cc
@@ -0,0 +1,72 @@
+#include "core/pch.hh"
+#include "core/cmdline.hh"
+
+// Valid options always start with OPTION_PREFIX, can contain
+// a bunch of OPTION_PREFIX'es inside and never end with one
+constexpr static char OPTION_PREFIX = '-';
+
+static std::unordered_map<std::string, std::string> options;
+
+static inline bool is_option_string(const std::string &string)
+{
+ if(string.find_last_of(OPTION_PREFIX) >= (string.size() - 1))
+ return false;
+ return string[0] == OPTION_PREFIX;
+}
+
+static inline std::string get_option(const std::string &string)
+{
+ std::size_t i;
+ for(i = 0; string[i] == OPTION_PREFIX; ++i);
+ return std::string(string.cbegin() + i, string.cend());
+}
+
+void cmdline::create(int argc, char **argv)
+{
+ for(int idx = 1; idx < argc; ++idx) {
+ std::string string = argv[idx];
+
+ if(!is_option_string(string)) {
+ spdlog::warn("cmdline: non-argument at {}: {}", idx, string);
+ continue;
+ }
+
+ auto option_string = get_option(string);
+ auto next_idx = idx + 1;
+
+ if(next_idx < argc) {
+ std::string argument = argv[next_idx];
+
+ if(!is_option_string(argument)) {
+ options.insert_or_assign(option_string, argument);
+ idx = next_idx;
+ continue;
+ }
+ }
+
+ // The option is either last or has no
+ // argument (happens when there is a valid
+ // option right next to the one we're parsing)
+ options.insert_or_assign(option_string, std::string());
+ }
+}
+
+void cmdline::insert(const char *option, const char *argument)
+{
+ if(argument == nullptr)
+ options.insert_or_assign(option, std::string());
+ else options.insert_or_assign(option, argument);
+}
+
+const char *cmdline::get(const char *option, const char *fallback)
+{
+ auto it = options.find(option);
+ if(it == options.cend())
+ return fallback;
+ return it->second.c_str();
+}
+
+bool cmdline::contains(const char *option)
+{
+ return options.count(option);
+}
diff --git a/core/cmdline.hh b/core/cmdline.hh
new file mode 100644
index 0000000..8ccfaec
--- /dev/null
+++ b/core/cmdline.hh
@@ -0,0 +1,13 @@
+#ifndef CORE_CMDLINE_HH
+#define CORE_CMDLINE_HH 1
+#pragma once
+
+namespace cmdline
+{
+void create(int argc, char **argv);
+void insert(const char *option, const char *argument = nullptr);
+const char *get(const char *option, const char *fallback = nullptr);
+bool contains(const char *option);
+} // namespace cmdline
+
+#endif /* CORE_CMDLINE_HH */
diff --git a/core/config.cc b/core/config.cc
new file mode 100644
index 0000000..94a8c1b
--- /dev/null
+++ b/core/config.cc
@@ -0,0 +1,179 @@
+#include "core/pch.hh"
+#include "core/config.hh"
+
+#include "core/cmdline.hh"
+#include "core/strtools.hh"
+#include "core/version.hh"
+
+ConfigBoolean::ConfigBoolean(bool default_value)
+{
+ m_value = default_value;
+ m_string = ConfigBoolean::to_string(default_value);
+}
+
+void ConfigBoolean::set(const char *value)
+{
+ m_value = ConfigBoolean::from_string(value);
+ m_string = ConfigBoolean::to_string(m_value);
+}
+
+const char *ConfigBoolean::get(void) const
+{
+ return m_string.c_str();
+}
+
+bool ConfigBoolean::get_value(void) const
+{
+ return m_value;
+}
+
+void ConfigBoolean::set_value(bool value)
+{
+ m_value = value;
+ m_string = ConfigBoolean::to_string(m_value);
+}
+
+const char *ConfigBoolean::to_string(bool value)
+{
+ if(value)
+ return "true";
+ return "false";
+}
+
+bool ConfigBoolean::from_string(const char *value)
+{
+ if(std::strcmp(value, "false") && !std::strcmp(value, "true"))
+ return true;
+ return false;
+}
+
+ConfigString::ConfigString(const char *default_value)
+{
+ m_value = default_value;
+}
+
+void ConfigString::set(const char *value)
+{
+ m_value = value;
+}
+
+const char *ConfigString::get(void) const
+{
+ return m_value.c_str();
+}
+
+void Config::load_cmdline(void)
+{
+ for(auto it : m_values) {
+ if(auto value = cmdline::get(it.first.c_str())) {
+ it.second->set(value);
+ }
+ }
+}
+
+bool Config::load_file(const char *path)
+{
+ if(auto file = PHYSFS_openRead(path)) {
+ auto source = std::string(PHYSFS_fileLength(file), char(0x00));
+ PHYSFS_readBytes(file, source.data(), source.size());
+ PHYSFS_close(file);
+
+ std::string line;
+ std::string kv_string;
+ std::istringstream stream(source);
+
+ while(std::getline(stream, line)) {
+ auto comment = line.find_first_of('#');
+
+ if(comment == std::string::npos)
+ kv_string = strtools::trim_whitespace(line);
+ else kv_string = strtools::trim_whitespace(line.substr(0, comment));
+
+ if(strtools::is_whitespace(kv_string)) {
+ // Ignore empty or commented out lines
+ continue;
+ }
+
+ auto separator = kv_string.find('=');
+
+ if(separator == std::string::npos) {
+ spdlog::warn("config: {}: invalid line: {}", path, line);
+ continue;
+ }
+
+ auto kv_name = strtools::trim_whitespace(kv_string.substr(0, separator));
+ auto kv_value = strtools::trim_whitespace(kv_string.substr(separator + 1));
+
+ auto kv_pair = m_values.find(kv_name);
+
+ if(kv_pair == m_values.cend()) {
+ spdlog::warn("config: {}: unknown key: {}", path, kv_name);
+ continue;
+ }
+
+ kv_pair->second->set(kv_value.c_str());
+ }
+
+ return true;
+ }
+
+ return false;
+}
+
+bool Config::save_file(const char *path) const
+{
+ std::ostringstream stream;
+
+ auto curtime = std::time(nullptr);
+
+ stream << "# Voxelius " << PROJECT_VERSION_STRING << " configuration file" << std::endl;
+ stream << "# Generated at: " << std::put_time(std::gmtime(&curtime), "%Y-%m-%d %H:%M:%S %z") << std::endl << std::endl;
+
+ for(const auto &it : m_values) {
+ stream << it.first << "=";
+ stream << it.second->get();
+ stream << std::endl;
+ }
+
+ if(auto file = PHYSFS_openWrite(path)) {
+ auto source = stream.str();
+ PHYSFS_writeBytes(file, source.data(), source.size());
+ PHYSFS_close(file);
+ return true;
+ }
+
+ return false;
+}
+
+bool Config::set_value(const char *name, const char *value)
+{
+ auto kv_pair = m_values.find(name);
+
+ if(kv_pair != m_values.cend()) {
+ kv_pair->second->set(value);
+ return true;
+ }
+
+ return false;
+}
+
+const char *Config::get_value(const char *name) const
+{
+ auto kv_pair = m_values.find(name);
+ if(kv_pair != m_values.cend())
+ return kv_pair->second->get();
+ return nullptr;
+}
+
+void Config::add_value(const char *name, IConfigValue &vref)
+{
+ m_values.insert_or_assign(name, &vref);
+}
+
+const IConfigValue *Config::find(const char *name) const
+{
+ auto kv_pair = m_values.find(name);
+ if(kv_pair != m_values.cend())
+ return kv_pair->second;
+ return nullptr;
+}
diff --git a/core/config.hh b/core/config.hh
new file mode 100644
index 0000000..ac9f08c
--- /dev/null
+++ b/core/config.hh
@@ -0,0 +1,181 @@
+#ifndef CORE_CONFIG_HH
+#define CORE_CONFIG_HH 1
+#pragma once
+
+class IConfigValue {
+public:
+ virtual ~IConfigValue(void) = default;
+ virtual void set(const char *value) = 0;
+ virtual const char *get(void) const = 0;
+};
+
+class ConfigBoolean final : public IConfigValue {
+public:
+ explicit ConfigBoolean(bool default_value = false);
+ virtual ~ConfigBoolean(void) = default;
+
+ virtual void set(const char *value) override;
+ virtual const char *get(void) const override;
+
+ bool get_value(void) const;
+ void set_value(bool value);
+
+private:
+ bool m_value;
+ std::string m_string;
+
+public:
+ static const char *to_string(bool value);
+ static bool from_string(const char *value);
+};
+
+template<typename T>
+class ConfigNumber : public IConfigValue {
+ static_assert(std::is_arithmetic_v<T>);
+
+public:
+ explicit ConfigNumber(T default_value = T(0));
+ explicit ConfigNumber(T default_value, T min_value, T max_value);
+ virtual ~ConfigNumber(void) = default;
+
+ virtual void set(const char *value) override;
+ virtual const char *get(void) const override;
+
+ T get_value(void) const;
+ void set_value(T value);
+
+ T get_min_value(void) const;
+ T get_max_value(void) const;
+ void set_limits(T min_value, T max_value);
+
+private:
+ T m_value;
+ T m_min_value;
+ T m_max_value;
+ std::string m_string;
+};
+
+class ConfigInt final : public ConfigNumber<int> {
+public:
+ using ConfigNumber<int>::ConfigNumber;
+};
+
+class ConfigFloat final : public ConfigNumber<float> {
+public:
+ using ConfigNumber<float>::ConfigNumber;
+};
+
+class ConfigUnsigned final : public ConfigNumber<unsigned int> {
+public:
+ using ConfigNumber<unsigned int>::ConfigNumber;
+};
+
+class ConfigUnsigned64 final : public ConfigNumber<std::uint64_t> {
+public:
+ using ConfigNumber<std::uint64_t>::ConfigNumber;
+};
+
+class ConfigSizeType final : public ConfigNumber<std::size_t> {
+public:
+ using ConfigNumber<std::size_t>::ConfigNumber;
+};
+
+class ConfigString final : public IConfigValue {
+public:
+ explicit ConfigString(const char *default_value);
+ virtual ~ConfigString(void) = default;
+
+ virtual void set(const char *value) override;
+ virtual const char *get(void) const override;
+
+private:
+ std::string m_value;
+};
+
+class Config final {
+public:
+ explicit Config(void) = default;
+ virtual ~Config(void) = default;
+
+ void load_cmdline(void);
+ bool load_file(const char *path);
+ bool save_file(const char *path) const;
+
+ bool set_value(const char *name, const char *value);
+ const char *get_value(const char *name) const;
+
+ void add_value(const char *name, IConfigValue &vref);
+
+ const IConfigValue *find(const char *name) const;
+
+private:
+ std::unordered_map<std::string, IConfigValue *> m_values;
+};
+
+template<typename T>
+inline ConfigNumber<T>::ConfigNumber(T default_value)
+{
+ m_value = default_value;
+ m_min_value = std::numeric_limits<T>::min();
+ m_max_value = std::numeric_limits<T>::max();
+ m_string = std::to_string(default_value);
+}
+
+template<typename T>
+inline ConfigNumber<T>::ConfigNumber(T default_value, T min_value, T max_value)
+{
+ m_value = default_value;
+ m_min_value = min_value;
+ m_max_value = max_value;
+ m_string = std::to_string(default_value);
+}
+
+template<typename T>
+inline void ConfigNumber<T>::set(const char *value)
+{
+ std::istringstream(value) >> m_value;
+ m_value = std::clamp(m_value, m_min_value, m_max_value);
+ m_string = std::to_string(m_value);
+}
+
+template<typename T>
+inline const char *ConfigNumber<T>::get(void) const
+{
+ return m_string.c_str();
+}
+
+template<typename T>
+inline T ConfigNumber<T>::get_value(void) const
+{
+ return m_value;
+}
+
+template<typename T>
+inline void ConfigNumber<T>::set_value(T value)
+{
+ m_value = std::clamp(value, m_min_value, m_max_value);
+ m_string = std::to_string(m_value);
+}
+
+template<typename T>
+inline T ConfigNumber<T>::get_min_value(void) const
+{
+ return m_min_value;
+}
+
+template<typename T>
+inline T ConfigNumber<T>::get_max_value(void) const
+{
+ return m_max_value;
+}
+
+template<typename T>
+inline void ConfigNumber<T>::set_limits(T min_value, T max_value)
+{
+ m_min_value = min_value;
+ m_max_value = max_value;
+ m_value = std::clamp(m_value, m_min_value, m_max_value);
+ m_string = std::to_string(m_value);
+}
+
+#endif /* CORE_CONFIG_HH */
diff --git a/core/constexpr.hh b/core/constexpr.hh
new file mode 100644
index 0000000..559f8d1
--- /dev/null
+++ b/core/constexpr.hh
@@ -0,0 +1,179 @@
+#ifndef CORE_CONSTEXPR_HH
+#define CORE_CONSTEXPR_HH 1
+#pragma once
+
+namespace cxpr
+{
+template<typename T>
+constexpr static inline const T abs(const T x);
+template<typename T, std::size_t L>
+constexpr static inline const std::size_t array_size(const T(&)[L]);
+template<typename T, typename F>
+constexpr static inline const T ceil(const F x);
+template<typename T>
+constexpr static inline const T degrees(const T x);
+template<typename T, typename F>
+constexpr static inline const T floor(const F x);
+template<typename T>
+constexpr static inline const T clamp(const T x, const T min, const T max);
+template<typename T, typename F>
+constexpr static inline const T lerp(const T x, const T y, const F a);
+template<typename T>
+constexpr static inline const T log2(const T x);
+template<typename T>
+constexpr static inline const T max(const T x, const T y);
+template<typename T>
+constexpr static inline const T min(const T x, const T y);
+template<typename T>
+constexpr static inline const T mod_signed(const T x, const T m);
+template<typename T>
+constexpr static inline const T pow2(const T x);
+template<typename T>
+constexpr static inline const T radians(const T x);
+template<typename T>
+constexpr static inline const bool range(const T x, const T min, const T max);
+template<typename T, typename F>
+constexpr static inline const T sign(const F x);
+template<typename T, typename F>
+constexpr static inline const T smoothstep(const T x, const T y, const F a);
+} // namespace cxpr
+
+template<typename T>
+constexpr static inline const T cxpr::abs(const T x)
+{
+ if(x < static_cast<T>(0))
+ return -x;
+ return x;
+}
+
+template<typename T, std::size_t L>
+constexpr static inline const std::size_t cxpr::array_size(const T(&)[L])
+{
+ return L;
+}
+
+template<typename T, typename F>
+constexpr static inline const T cxpr::ceil(const F x)
+{
+ static_assert(std::is_integral_v<T>);
+ static_assert(std::is_floating_point_v<F>);
+
+ const T ival = static_cast<T>(x);
+ if(ival < x)
+ return ival + static_cast<T>(1);
+ return ival;
+}
+
+template<typename T>
+constexpr static inline const T cxpr::degrees(const T x)
+{
+ return x * static_cast<T>(180.0) / static_cast<T>(M_PI);
+}
+
+template<typename T, typename F>
+constexpr static inline const T cxpr::floor(const F x)
+{
+ static_assert(std::is_integral_v<T>);
+ static_assert(std::is_floating_point_v<F>);
+
+ const T ival = static_cast<T>(x);
+ if(ival > x)
+ return ival - static_cast<T>(1);
+ return ival;
+}
+
+template<typename T>
+constexpr static inline const T cxpr::clamp(const T x, const T min, const T max)
+{
+ if(x < min)
+ return min;
+ if(x > max)
+ return max;
+ return x;
+}
+
+template<typename T, typename F>
+constexpr static inline const T cxpr::lerp(const T x, const T y, const F a)
+{
+ static_assert(std::is_arithmetic_v<T>);
+ static_assert(std::is_floating_point_v<F>);
+ return static_cast<T>(static_cast<F>(x) * (static_cast<F>(1.0f) - a) + static_cast<F>(y) * a);
+}
+
+template<typename T>
+constexpr static inline const T cxpr::log2(const T x)
+{
+ if(x < 2)
+ return 0;
+ return cxpr::log2<T>((x + 1) >> 1) + 1;
+}
+
+template<typename T>
+constexpr static inline const T cxpr::max(const T x, const T y)
+{
+ if(x < y)
+ return y;
+ return x;
+}
+
+template<typename T>
+constexpr static inline const T cxpr::min(const T x, const T y)
+{
+ if(x > y)
+ return y;
+ return x;
+}
+
+template<typename T>
+constexpr static inline const T cxpr::mod_signed(const T x, const T m)
+{
+ static_assert(std::is_signed_v<T>);
+ static_assert(std::is_integral_v<T>);
+ const T result = static_cast<T>(x % m);
+ if(result < T(0))
+ return result + m;
+ return result;
+}
+
+template<typename T>
+constexpr static inline const T cxpr::pow2(const T x)
+{
+ T value = static_cast<T>(1);
+ while(value < x)
+ value *= static_cast<T>(2);
+ return value;
+}
+
+template<typename T>
+constexpr static inline const T cxpr::radians(const T x)
+{
+ return x * static_cast<T>(M_PI) / static_cast<T>(180.0);
+}
+
+template<typename T>
+constexpr static inline const bool cxpr::range(const T x, const T min, const T max)
+{
+ return ((x >= min) && (x <= max));
+}
+
+template<typename T, typename F>
+constexpr static inline const T cxpr::sign(const F x)
+{
+ if(x < F(0))
+ return T(-1);
+ if(x > F(0))
+ return T(+1);
+ return T(0);
+}
+
+template<typename T, typename F>
+constexpr static inline const T cxpr::smoothstep(const T x, const T y, const F a)
+{
+ static_assert(std::is_arithmetic_v<T>);
+ static_assert(std::is_floating_point_v<F>);
+
+ const F t = cxpr::clamp<F>((a - x) / (y - x), F(0), F(1));
+ return static_cast<T>(t * t * (F(3) - F(2) * t));
+}
+
+#endif /* CORE_CONSTEXPR_HH */
diff --git a/core/crc64.cc b/core/crc64.cc
new file mode 100644
index 0000000..dee88f3
--- /dev/null
+++ b/core/crc64.cc
@@ -0,0 +1,93 @@
+#include "core/pch.hh"
+#include "core/crc64.hh"
+
+// The lookup table for CRC64 checksum; this lookup
+// table is generated using ECMA-182 compilant parameters:
+// - Polynomial: `0x42F0E1EBA9EA3693`
+// - Initial value: `0x0000000000000000`
+// - Final xor: `0x0000000000000000`
+// CRC Calculator: https://www.sunshine2k.de/coding/javascript/crc/crc_js.html
+static const std::uint64_t crc_table[256] = {
+ 0x0000000000000000, 0x42F0E1EBA9EA3693, 0x85E1C3D753D46D26, 0xC711223CFA3E5BB5,
+ 0x493366450E42ECDF, 0x0BC387AEA7A8DA4C, 0xCCD2A5925D9681F9, 0x8E224479F47CB76A,
+ 0x9266CC8A1C85D9BE, 0xD0962D61B56FEF2D, 0x17870F5D4F51B498, 0x5577EEB6E6BB820B,
+ 0xDB55AACF12C73561, 0x99A54B24BB2D03F2, 0x5EB4691841135847, 0x1C4488F3E8F96ED4,
+ 0x663D78FF90E185EF, 0x24CD9914390BB37C, 0xE3DCBB28C335E8C9, 0xA12C5AC36ADFDE5A,
+ 0x2F0E1EBA9EA36930, 0x6DFEFF5137495FA3, 0xAAEFDD6DCD770416, 0xE81F3C86649D3285,
+ 0xF45BB4758C645C51, 0xB6AB559E258E6AC2, 0x71BA77A2DFB03177, 0x334A9649765A07E4,
+ 0xBD68D2308226B08E, 0xFF9833DB2BCC861D, 0x388911E7D1F2DDA8, 0x7A79F00C7818EB3B,
+ 0xCC7AF1FF21C30BDE, 0x8E8A101488293D4D, 0x499B3228721766F8, 0x0B6BD3C3DBFD506B,
+ 0x854997BA2F81E701, 0xC7B97651866BD192, 0x00A8546D7C558A27, 0x4258B586D5BFBCB4,
+ 0x5E1C3D753D46D260, 0x1CECDC9E94ACE4F3, 0xDBFDFEA26E92BF46, 0x990D1F49C77889D5,
+ 0x172F5B3033043EBF, 0x55DFBADB9AEE082C, 0x92CE98E760D05399, 0xD03E790CC93A650A,
+ 0xAA478900B1228E31, 0xE8B768EB18C8B8A2, 0x2FA64AD7E2F6E317, 0x6D56AB3C4B1CD584,
+ 0xE374EF45BF6062EE, 0xA1840EAE168A547D, 0x66952C92ECB40FC8, 0x2465CD79455E395B,
+ 0x3821458AADA7578F, 0x7AD1A461044D611C, 0xBDC0865DFE733AA9, 0xFF3067B657990C3A,
+ 0x711223CFA3E5BB50, 0x33E2C2240A0F8DC3, 0xF4F3E018F031D676, 0xB60301F359DBE0E5,
+ 0xDA050215EA6C212F, 0x98F5E3FE438617BC, 0x5FE4C1C2B9B84C09, 0x1D14202910527A9A,
+ 0x93366450E42ECDF0, 0xD1C685BB4DC4FB63, 0x16D7A787B7FAA0D6, 0x5427466C1E109645,
+ 0x4863CE9FF6E9F891, 0x0A932F745F03CE02, 0xCD820D48A53D95B7, 0x8F72ECA30CD7A324,
+ 0x0150A8DAF8AB144E, 0x43A04931514122DD, 0x84B16B0DAB7F7968, 0xC6418AE602954FFB,
+ 0xBC387AEA7A8DA4C0, 0xFEC89B01D3679253, 0x39D9B93D2959C9E6, 0x7B2958D680B3FF75,
+ 0xF50B1CAF74CF481F, 0xB7FBFD44DD257E8C, 0x70EADF78271B2539, 0x321A3E938EF113AA,
+ 0x2E5EB66066087D7E, 0x6CAE578BCFE24BED, 0xABBF75B735DC1058, 0xE94F945C9C3626CB,
+ 0x676DD025684A91A1, 0x259D31CEC1A0A732, 0xE28C13F23B9EFC87, 0xA07CF2199274CA14,
+ 0x167FF3EACBAF2AF1, 0x548F120162451C62, 0x939E303D987B47D7, 0xD16ED1D631917144,
+ 0x5F4C95AFC5EDC62E, 0x1DBC74446C07F0BD, 0xDAAD56789639AB08, 0x985DB7933FD39D9B,
+ 0x84193F60D72AF34F, 0xC6E9DE8B7EC0C5DC, 0x01F8FCB784FE9E69, 0x43081D5C2D14A8FA,
+ 0xCD2A5925D9681F90, 0x8FDAB8CE70822903, 0x48CB9AF28ABC72B6, 0x0A3B7B1923564425,
+ 0x70428B155B4EAF1E, 0x32B26AFEF2A4998D, 0xF5A348C2089AC238, 0xB753A929A170F4AB,
+ 0x3971ED50550C43C1, 0x7B810CBBFCE67552, 0xBC902E8706D82EE7, 0xFE60CF6CAF321874,
+ 0xE224479F47CB76A0, 0xA0D4A674EE214033, 0x67C58448141F1B86, 0x253565A3BDF52D15,
+ 0xAB1721DA49899A7F, 0xE9E7C031E063ACEC, 0x2EF6E20D1A5DF759, 0x6C0603E6B3B7C1CA,
+ 0xF6FAE5C07D3274CD, 0xB40A042BD4D8425E, 0x731B26172EE619EB, 0x31EBC7FC870C2F78,
+ 0xBFC9838573709812, 0xFD39626EDA9AAE81, 0x3A28405220A4F534, 0x78D8A1B9894EC3A7,
+ 0x649C294A61B7AD73, 0x266CC8A1C85D9BE0, 0xE17DEA9D3263C055, 0xA38D0B769B89F6C6,
+ 0x2DAF4F0F6FF541AC, 0x6F5FAEE4C61F773F, 0xA84E8CD83C212C8A, 0xEABE6D3395CB1A19,
+ 0x90C79D3FEDD3F122, 0xD2377CD44439C7B1, 0x15265EE8BE079C04, 0x57D6BF0317EDAA97,
+ 0xD9F4FB7AE3911DFD, 0x9B041A914A7B2B6E, 0x5C1538ADB04570DB, 0x1EE5D94619AF4648,
+ 0x02A151B5F156289C, 0x4051B05E58BC1E0F, 0x87409262A28245BA, 0xC5B073890B687329,
+ 0x4B9237F0FF14C443, 0x0962D61B56FEF2D0, 0xCE73F427ACC0A965, 0x8C8315CC052A9FF6,
+ 0x3A80143F5CF17F13, 0x7870F5D4F51B4980, 0xBF61D7E80F251235, 0xFD913603A6CF24A6,
+ 0x73B3727A52B393CC, 0x31439391FB59A55F, 0xF652B1AD0167FEEA, 0xB4A25046A88DC879,
+ 0xA8E6D8B54074A6AD, 0xEA16395EE99E903E, 0x2D071B6213A0CB8B, 0x6FF7FA89BA4AFD18,
+ 0xE1D5BEF04E364A72, 0xA3255F1BE7DC7CE1, 0x64347D271DE22754, 0x26C49CCCB40811C7,
+ 0x5CBD6CC0CC10FAFC, 0x1E4D8D2B65FACC6F, 0xD95CAF179FC497DA, 0x9BAC4EFC362EA149,
+ 0x158E0A85C2521623, 0x577EEB6E6BB820B0, 0x906FC95291867B05, 0xD29F28B9386C4D96,
+ 0xCEDBA04AD0952342, 0x8C2B41A1797F15D1, 0x4B3A639D83414E64, 0x09CA82762AAB78F7,
+ 0x87E8C60FDED7CF9D, 0xC51827E4773DF90E, 0x020905D88D03A2BB, 0x40F9E43324E99428,
+ 0x2CFFE7D5975E55E2, 0x6E0F063E3EB46371, 0xA91E2402C48A38C4, 0xEBEEC5E96D600E57,
+ 0x65CC8190991CB93D, 0x273C607B30F68FAE, 0xE02D4247CAC8D41B, 0xA2DDA3AC6322E288,
+ 0xBE992B5F8BDB8C5C, 0xFC69CAB42231BACF, 0x3B78E888D80FE17A, 0x7988096371E5D7E9,
+ 0xF7AA4D1A85996083, 0xB55AACF12C735610, 0x724B8ECDD64D0DA5, 0x30BB6F267FA73B36,
+ 0x4AC29F2A07BFD00D, 0x08327EC1AE55E69E, 0xCF235CFD546BBD2B, 0x8DD3BD16FD818BB8,
+ 0x03F1F96F09FD3CD2, 0x41011884A0170A41, 0x86103AB85A2951F4, 0xC4E0DB53F3C36767,
+ 0xD8A453A01B3A09B3, 0x9A54B24BB2D03F20, 0x5D45907748EE6495, 0x1FB5719CE1045206,
+ 0x919735E51578E56C, 0xD367D40EBC92D3FF, 0x1476F63246AC884A, 0x568617D9EF46BED9,
+ 0xE085162AB69D5E3C, 0xA275F7C11F7768AF, 0x6564D5FDE549331A, 0x279434164CA30589,
+ 0xA9B6706FB8DFB2E3, 0xEB46918411358470, 0x2C57B3B8EB0BDFC5, 0x6EA7525342E1E956,
+ 0x72E3DAA0AA188782, 0x30133B4B03F2B111, 0xF7021977F9CCEAA4, 0xB5F2F89C5026DC37,
+ 0x3BD0BCE5A45A6B5D, 0x79205D0E0DB05DCE, 0xBE317F32F78E067B, 0xFCC19ED95E6430E8,
+ 0x86B86ED5267CDBD3, 0xC4488F3E8F96ED40, 0x0359AD0275A8B6F5, 0x41A94CE9DC428066,
+ 0xCF8B0890283E370C, 0x8D7BE97B81D4019F, 0x4A6ACB477BEA5A2A, 0x089A2AACD2006CB9,
+ 0x14DEA25F3AF9026D, 0x562E43B4931334FE, 0x913F6188692D6F4B, 0xD3CF8063C0C759D8,
+ 0x5DEDC41A34BBEEB2, 0x1F1D25F19D51D821, 0xD80C07CD676F8394, 0x9AFCE626CE85B507,
+};
+
+std::uint64_t crc64::get(const void *buffer, std::size_t size, std::uint64_t combine)
+{
+ auto data = reinterpret_cast<const std::uint8_t *>(buffer);
+ for(std::size_t i = 0; i < size; ++i)
+ combine = crc_table[((combine >> 56) ^ data[i]) & 0xFF] ^ (combine << 8);
+ return combine;
+}
+
+std::uint64_t crc64::get(const std::vector<std::byte> &buffer, std::uint64_t combine)
+{
+ return crc64::get(buffer.data(), buffer.size(), combine);
+}
+
+std::uint64_t crc64::get(const std::string &buffer, std::uint64_t combine)
+{
+ return crc64::get(buffer.data(), buffer.size(), combine);
+}
diff --git a/core/crc64.hh b/core/crc64.hh
new file mode 100644
index 0000000..5528bc5
--- /dev/null
+++ b/core/crc64.hh
@@ -0,0 +1,12 @@
+#ifndef CORE_CRC64_HH
+#define CORE_CRC64_HH 1
+#pragma once
+
+namespace crc64
+{
+std::uint64_t get(const void *buffer, std::size_t size, std::uint64_t combine = UINT64_C(0));
+std::uint64_t get(const std::vector<std::byte> &buffer, std::uint64_t combine = UINT64_C(0));
+std::uint64_t get(const std::string &buffer, std::uint64_t combine = UINT64_C(0));
+} // namespace crc64
+
+#endif /* CORE_CRC64_HH */
diff --git a/core/epoch.cc b/core/epoch.cc
new file mode 100644
index 0000000..398b7fd
--- /dev/null
+++ b/core/epoch.cc
@@ -0,0 +1,38 @@
+#include "core/pch.hh"
+#include "core/epoch.hh"
+
+std::uint64_t epoch::seconds(void)
+{
+ const auto elapsed = std::chrono::system_clock::now().time_since_epoch();
+ return static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::seconds>(elapsed).count());
+}
+
+std::uint64_t epoch::milliseconds(void)
+{
+ const auto elapsed = std::chrono::system_clock::now().time_since_epoch();
+ return static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::milliseconds>(elapsed).count());
+}
+
+std::uint64_t epoch::microseconds(void)
+{
+ const auto elapsed = std::chrono::system_clock::now().time_since_epoch();
+ return static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count());
+}
+
+std::int64_t epoch::signed_seconds(void)
+{
+ const auto elapsed = std::chrono::system_clock::now().time_since_epoch();
+ return static_cast<std::int64_t>(std::chrono::duration_cast<std::chrono::seconds>(elapsed).count());
+}
+
+std::int64_t epoch::signed_milliseconds(void)
+{
+ const auto elapsed = std::chrono::system_clock::now().time_since_epoch();
+ return static_cast<std::int64_t>(std::chrono::duration_cast<std::chrono::milliseconds>(elapsed).count());
+}
+
+std::int64_t epoch::signed_microseconds(void)
+{
+ const auto elapsed = std::chrono::system_clock::now().time_since_epoch();
+ return static_cast<std::int64_t>(std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count());
+}
diff --git a/core/epoch.hh b/core/epoch.hh
new file mode 100644
index 0000000..f590f27
--- /dev/null
+++ b/core/epoch.hh
@@ -0,0 +1,19 @@
+#ifndef CORE_EPOCH_HH
+#define CORE_EPOCH_HH 1
+#pragma once
+
+namespace epoch
+{
+std::uint64_t seconds(void);
+std::uint64_t milliseconds(void);
+std::uint64_t microseconds(void);
+} // namespace epoch
+
+namespace epoch
+{
+std::int64_t signed_seconds(void);
+std::int64_t signed_milliseconds(void);
+std::int64_t signed_microseconds(void);
+} // namespace epoch
+
+#endif /* CORE_EPOCH_HH */
diff --git a/core/feature.hh.in b/core/feature.hh.in
new file mode 100644
index 0000000..3578416
--- /dev/null
+++ b/core/feature.hh.in
@@ -0,0 +1,5 @@
+#ifndef CORE_FEATURE_HH
+#define CORE_FEATURE_HH 1
+#pragma once
+
+#endif /* CORE_FEATURE_HH */
diff --git a/core/floathacks.hh b/core/floathacks.hh
new file mode 100644
index 0000000..2c6a3e1
--- /dev/null
+++ b/core/floathacks.hh
@@ -0,0 +1,50 @@
+#ifndef CORE_FLOATHACKS_HH
+#define CORE_FLOATHACKS_HH 1
+#pragma once
+
+namespace floathacks
+{
+static inline float int32_to_float(const std::int32_t value);
+static inline float uint32_to_float(const std::uint32_t value);
+static inline std::int32_t float_to_int32(const float value);
+static inline std::uint32_t float_to_uint32(const float value);
+} // namespace floathacks
+
+static inline float floathacks::int32_to_float(const std::int32_t value)
+{
+ static_assert(std::numeric_limits<float>::is_iec559);
+ static_assert(sizeof(std::int32_t) == sizeof(float));
+ union { std::int32_t src; float dst; } hack;
+ hack.src = value;
+ return hack.dst;
+
+}
+
+static inline float floathacks::uint32_to_float(const std::uint32_t value)
+{
+ static_assert(std::numeric_limits<float>::is_iec559);
+ static_assert(sizeof(std::uint32_t) == sizeof(float));
+ union { std::uint32_t src; float dst; } hack;
+ hack.src = value;
+ return hack.dst;
+}
+
+static inline std::int32_t floathacks::float_to_int32(const float value)
+{
+ static_assert(std::numeric_limits<float>::is_iec559);
+ static_assert(sizeof(std::int32_t) == sizeof(float));
+ union { float src; std::int32_t dst; } hack;
+ hack.src = value;
+ return hack.dst;
+}
+
+static inline std::uint32_t floathacks::float_to_uint32(const float value)
+{
+ static_assert(std::numeric_limits<float>::is_iec559);
+ static_assert(sizeof(std::uint32_t) == sizeof(float));
+ union { float src; std::uint32_t dst; } hack;
+ hack.src = value;
+ return hack.dst;
+}
+
+#endif /* CORE_FLOATHACKS_HH */
diff --git a/core/image.cc b/core/image.cc
new file mode 100644
index 0000000..0c9add5
--- /dev/null
+++ b/core/image.cc
@@ -0,0 +1,104 @@
+#include "core/pch.hh"
+#include "core/image.hh"
+
+#include "core/resource.hh"
+
+static emhash8::HashMap<std::string, resource_ptr<Image>> resource_map;
+
+static int stbi_physfs_read(void *context, char *data, int size)
+{
+ return PHYSFS_readBytes(reinterpret_cast<PHYSFS_File *>(context), data, size);
+}
+
+static void stbi_physfs_skip(void *context, int count)
+{
+ auto file = reinterpret_cast<PHYSFS_File *>(context);
+ PHYSFS_seek(file, PHYSFS_tell(file) + count);
+}
+
+static int stbi_physfs_eof(void *context)
+{
+ return PHYSFS_eof(reinterpret_cast<PHYSFS_File *>(context));
+}
+
+template<>
+resource_ptr<Image> resource::load<Image>(const char *name, unsigned int flags)
+{
+ auto it = resource_map.find(name);
+
+ if(it != resource_map.cend()) {
+ // Return an existing resource
+ return it->second;
+ }
+
+ auto file = PHYSFS_openRead(name);
+
+ if(file == nullptr) {
+ spdlog::warn("resource: {}: {}", name, PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode()));
+ return nullptr;
+ }
+
+ if(flags & IMAGE_LOAD_FLIP)
+ stbi_set_flip_vertically_on_load(true);
+ else stbi_set_flip_vertically_on_load(false);
+
+ stbi_io_callbacks callbacks;
+ callbacks.read = &stbi_physfs_read;
+ callbacks.skip = &stbi_physfs_skip;
+ callbacks.eof = &stbi_physfs_eof;
+
+ auto new_resource = std::make_shared<Image>();
+
+ if(flags & IMAGE_LOAD_GRAY)
+ new_resource->pixels = stbi_load_from_callbacks(&callbacks, file, &new_resource->size.x, &new_resource->size.y, nullptr, STBI_grey);
+ else new_resource->pixels = stbi_load_from_callbacks(&callbacks, file, &new_resource->size.x, &new_resource->size.y, nullptr, STBI_rgb_alpha);
+
+ PHYSFS_close(file);
+
+ if(new_resource->pixels == nullptr) {
+ spdlog::warn("resource: {}: {}", name, stbi_failure_reason());
+ return nullptr;
+ }
+
+ if(new_resource->size.x <= 0 || new_resource->size.y <= 0) {
+ spdlog::warn("resource {}: invalid dimensions", name);
+ stbi_image_free(new_resource->pixels);
+ return nullptr;
+ }
+
+ return resource_map.insert_or_assign(name, new_resource).first->second;
+}
+
+template<>
+void resource::hard_cleanup<Image>(void)
+{
+ for(const auto &it : resource_map) {
+ if(it.second.use_count() > 1L)
+ spdlog::warn("resource: zombie resource [Image] {} [use_count={}]", it.first, it.second.use_count());
+ else spdlog::debug("resource: releasing [Image] {}", it.first);
+
+ stbi_image_free(it.second->pixels);
+ }
+
+ resource_map.clear();
+}
+
+template<>
+void resource::soft_cleanup<Image>(void)
+{
+ auto iter = resource_map.cbegin();
+
+ while(iter != resource_map.cend()) {
+ if(iter->second.use_count() == 1L) {
+ spdlog::debug("resource: releasing [Image] {}", iter->first);
+
+ stbi_image_free(iter->second->pixels);
+
+ iter = resource_map.erase(iter);
+
+ continue;
+ }
+
+ iter = std::next(iter);
+ }
+}
diff --git a/core/image.hh b/core/image.hh
new file mode 100644
index 0000000..0b13ba0
--- /dev/null
+++ b/core/image.hh
@@ -0,0 +1,13 @@
+#ifndef CORE_IMAGE_HH
+#define CORE_IMAGE_HH 1
+#pragma once
+
+constexpr static unsigned int IMAGE_LOAD_GRAY = 0x0001U;
+constexpr static unsigned int IMAGE_LOAD_FLIP = 0x0002U;
+
+struct Image final {
+ stbi_uc *pixels;
+ glm::ivec2 size;
+};
+
+#endif /* CORE_IMAGE_HH */
diff --git a/core/macros.hh b/core/macros.hh
new file mode 100644
index 0000000..def52da
--- /dev/null
+++ b/core/macros.hh
@@ -0,0 +1,7 @@
+#ifndef CORE_MACROS_HH
+#define CORE_MACROS_HH 1
+#pragma once
+
+#define DECLARE_DEFAULT_CTOR(type) public: type(void) = default
+
+#endif /* CORE_MACROS_HH */
diff --git a/core/pch.hh b/core/pch.hh
new file mode 100644
index 0000000..a2199a2
--- /dev/null
+++ b/core/pch.hh
@@ -0,0 +1,49 @@
+#ifndef CORE_PCH_HH
+#define CORE_PCH_HH 1
+#pragma once
+
+#include <cinttypes>
+#include <cmath>
+#include <cstdarg>
+#include <cstddef>
+#include <cstdint>
+
+#include <algorithm>
+#include <chrono>
+#include <filesystem>
+#include <iostream>
+#include <limits>
+#include <mutex>
+#include <random>
+#include <sstream>
+#include <stdexcept>
+#include <thread>
+#include <type_traits>
+#include <unordered_map>
+#include <unordered_set>
+#include <vector>
+
+#include <emhash/hash_table8.hpp>
+
+#include <enet/enet.h>
+
+#include <glm/fwd.hpp>
+
+#include <glm/mat4x4.hpp>
+#include <glm/vec2.hpp>
+#include <glm/vec3.hpp>
+#include <glm/vec4.hpp>
+
+#include <glm/gtc/quaternion.hpp>
+#include <glm/gtc/matrix_transform.hpp>
+#include <glm/gtc/type_ptr.hpp>
+
+#include <physfs.h>
+
+#include <spdlog/spdlog.h>
+#include <spdlog/fmt/fmt.h>
+
+#include <stb_image.h>
+#include <stb_image_write.h>
+
+#endif /* CORE_PCH_HH */
diff --git a/core/randomizer.hh b/core/randomizer.hh
new file mode 100644
index 0000000..62bdf9b
--- /dev/null
+++ b/core/randomizer.hh
@@ -0,0 +1,57 @@
+#ifndef CORE_RANDOMIZER_HH
+#define CORE_RANDOMIZER_HH 1
+#pragma once
+
+template<typename T>
+class Randomizer final {
+public:
+ explicit Randomizer(void);
+ explicit Randomizer(std::uint64_t seed);
+ virtual ~Randomizer(void) = default;
+ void add(const T &value);
+ const T &get(void);
+ void clear(void);
+
+private:
+ std::vector<T> m_vector;
+ std::mt19937_64 m_twister;
+ std::uniform_int_distribution<std::size_t> m_dist;
+};
+
+template<typename T>
+inline Randomizer<T>::Randomizer(void)
+{
+ m_vector.clear();
+ m_twister.seed(std::random_device()());
+ m_dist = std::uniform_int_distribution<std::size_t>(0, 0);
+}
+
+template<typename T>
+inline Randomizer<T>::Randomizer(std::uint64_t seed)
+{
+ m_vector.clear();
+ m_twister.seed(seed);
+ m_dist = std::uniform_int_distribution<std::size_t>(0, 0);
+}
+
+template<typename T>
+inline void Randomizer<T>::add(const T &value)
+{
+ m_vector.push_back(value);
+ m_dist = std::uniform_int_distribution<std::size_t>(0, m_vector.size() - 1);
+}
+
+template<typename T>
+inline const T &Randomizer<T>::get(void)
+{
+ return m_vector.at(m_dist(m_twister));
+}
+
+template<typename T>
+inline void Randomizer<T>::clear(void)
+{
+ m_vector.clear();
+ m_dist = std::uniform_int_distribution<std::size_t>(0, 0);
+}
+
+#endif /* CORE_RANDOMIZER_HH */
diff --git a/core/resource.hh b/core/resource.hh
new file mode 100644
index 0000000..6f73ca3
--- /dev/null
+++ b/core/resource.hh
@@ -0,0 +1,18 @@
+#ifndef CORE_RESOURCE_HH
+#define CORE_RESOURCE_HH 1
+#pragma once
+
+template<typename T>
+using resource_ptr = std::shared_ptr<const T>;
+
+namespace resource
+{
+template<typename T>
+resource_ptr<T> load(const char *name, unsigned int flags = 0U);
+template<typename T>
+void hard_cleanup(void);
+template<typename T>
+void soft_cleanup(void);
+} // namespace resource
+
+#endif /* CORE_RESOURCE_HH */
diff --git a/core/strtools.cc b/core/strtools.cc
new file mode 100644
index 0000000..ebb8f73
--- /dev/null
+++ b/core/strtools.cc
@@ -0,0 +1,47 @@
+#include "core/pch.hh"
+#include "core/strtools.hh"
+
+constexpr static const char *WHITESPACE_CHARS = " \t\r\n";
+
+bool strtools::is_whitespace(const std::string &string)
+{
+ if(string.find_first_not_of(WHITESPACE_CHARS) == std::string::npos)
+ return true;
+ if((string.size() == 1) && string[0] == 0x00)
+ return true;
+ return string.empty();
+}
+
+std::string strtools::join(const std::vector<std::string> &strings, const std::string &separator)
+{
+ std::ostringstream stream;
+ for(const std::string &str : strings)
+ stream << str << separator;
+ return stream.str();
+}
+
+std::vector<std::string> strtools::split(const std::string &string, const std::string &separator)
+{
+ std::size_t pos = 0;
+ std::size_t prev = 0;
+ std::vector<std::string> result;
+
+ while((pos = string.find(separator, prev)) != std::string::npos) {
+ result.push_back(string.substr(prev, pos - prev));
+ prev = pos + separator.length();
+ }
+
+ if(prev <= string.length())
+ result.push_back(string.substr(prev, string.length() - prev));
+ return result;
+}
+
+std::string strtools::trim_whitespace(const std::string &string)
+{
+ auto su = string.find_first_not_of(WHITESPACE_CHARS);
+ auto sv = string.find_last_not_of(WHITESPACE_CHARS);
+
+ if(su == std::string::npos)
+ return std::string();
+ return string.substr(su, sv - su + 1);
+}
diff --git a/core/strtools.hh b/core/strtools.hh
new file mode 100644
index 0000000..0d2aaf8
--- /dev/null
+++ b/core/strtools.hh
@@ -0,0 +1,21 @@
+#ifndef CORE_STRTOOLS_HH
+#define CORE_STRTOOLS_HH 1
+#pragma once
+
+namespace strtools
+{
+bool is_whitespace(const std::string &string);
+} // namespace strtools
+
+namespace strtools
+{
+std::string join(const std::vector<std::string> &strings, const std::string &separator);
+std::vector<std::string> split(const std::string &string, const std::string &separator);
+} // namespace strtools
+
+namespace strtools
+{
+std::string trim_whitespace(const std::string &string);
+} // namespace strtools
+
+#endif /* CORE_STRTOOLS_HH */
diff --git a/core/version.hh.in b/core/version.hh.in
new file mode 100644
index 0000000..bddd283
--- /dev/null
+++ b/core/version.hh.in
@@ -0,0 +1,12 @@
+#ifndef CORE_VERSION_HH
+#define CORE_VERSION_HH 1
+#pragma once
+
+#define PROJECT_VERSION_MAJOR ${PROJECT_VERSION_MAJOR}
+#define PROJECT_VERSION_MINOR ${PROJECT_VERSION_MINOR}
+#define PROJECT_VERSION_PATCH ${PROJECT_VERSION_PATCH}
+#define PROJECT_VERSION_TWEAK ${PROJECT_VERSION_TWEAK}
+
+#define PROJECT_VERSION_STRING "${PROJECT_VERSION}"
+
+#endif /* CORE_VERSION_HH */