summaryrefslogtreecommitdiffstats
path: root/core
diff options
context:
space:
mode:
authoruntodesu <kirill@untode.su>2025-07-01 03:08:39 +0500
committeruntodesu <kirill@untode.su>2025-07-01 03:08:39 +0500
commit458e0005690ea9d579588a0a12368fc2c2c9a93a (patch)
tree588a9ca6cb3c76d9193b5bd4601d64f0e50e8c8c /core
parentc7b0c8e0286a1b2bb7ec55e579137dfc3b22eeb9 (diff)
downloadvoxelius-458e0005690ea9d579588a0a12368fc2c2c9a93a.tar.bz2
voxelius-458e0005690ea9d579588a0a12368fc2c2c9a93a.zip
I hyper-focued on refactoring again
- I put a cool-sounding "we are number one" remix on repeat and straight up grinded the entire repository to a better state until 03:09 AM. I guess I have something wrong in my brain that makes me do this shit
Diffstat (limited to 'core')
-rw-r--r--core/CMakeLists.txt22
-rw-r--r--core/config/CMakeLists.txt7
-rw-r--r--core/config/boolean.cc46
-rw-r--r--core/config/boolean.hh29
-rw-r--r--core/config/ivalue.hh15
-rw-r--r--core/config/number.hh129
-rw-r--r--core/config/string.cc18
-rw-r--r--core/config/string.hh22
-rw-r--r--core/io/CMakeLists.txt7
-rw-r--r--core/io/buffer.cc208
-rw-r--r--core/io/buffer.hh260
-rw-r--r--core/io/cmdline.cc83
-rw-r--r--core/io/cmdline.hh13
-rw-r--r--core/io/config_map.cc131
-rw-r--r--core/io/config_map.hh33
-rw-r--r--core/math/CMakeLists.txt11
-rw-r--r--core/math/aabb.cc59
-rw-r--r--core/math/aabb.hh32
-rw-r--r--core/math/angles.hh107
-rw-r--r--core/math/concepts.hh15
-rw-r--r--core/math/constexpr.hh191
-rw-r--r--core/math/crc64.cc286
-rw-r--r--core/math/crc64.hh12
-rw-r--r--core/math/floathacks.hh56
-rw-r--r--core/math/randomizer.hh60
-rw-r--r--core/math/vectors.hh47
-rw-r--r--core/pch.hh50
-rw-r--r--core/resource/CMakeLists.txt6
-rw-r--r--core/resource/binfile.cc70
-rw-r--r--core/resource/binfile.hh10
-rw-r--r--core/resource/image.cc112
-rw-r--r--core/resource/image.hh13
-rw-r--r--core/resource/resource.hh18
-rw-r--r--core/utils/CMakeLists.txt5
-rw-r--r--core/utils/epoch.cc39
-rw-r--r--core/utils/epoch.hh19
-rw-r--r--core/utils/string.cc54
-rw-r--r--core/utils/string.hh21
-rw-r--r--core/version.cc12
-rw-r--r--core/version.cc.in12
-rw-r--r--core/version.hh12
41 files changed, 2352 insertions, 0 deletions
diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt
new file mode 100644
index 0000000..0710274
--- /dev/null
+++ b/core/CMakeLists.txt
@@ -0,0 +1,22 @@
+configure_file("${CMAKE_CURRENT_LIST_DIR}/version.cc.in" "${CMAKE_CURRENT_LIST_DIR}/version.cc")
+
+add_library(core STATIC
+ "${CMAKE_CURRENT_LIST_DIR}/pch.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/version.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/version.hh")
+target_compile_features(core PUBLIC cxx_std_20)
+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 emhash glm physfs spdlog stb)
+
+add_subdirectory(config)
+add_subdirectory(io)
+add_subdirectory(math)
+add_subdirectory(resource)
+add_subdirectory(utils)
+
+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()
diff --git a/core/config/CMakeLists.txt b/core/config/CMakeLists.txt
new file mode 100644
index 0000000..39752d4
--- /dev/null
+++ b/core/config/CMakeLists.txt
@@ -0,0 +1,7 @@
+target_sources(core PRIVATE
+ "${CMAKE_CURRENT_LIST_DIR}/boolean.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/boolean.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/ivalue.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/number.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/string.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/string.hh")
diff --git a/core/config/boolean.cc b/core/config/boolean.cc
new file mode 100644
index 0000000..a8d670c
--- /dev/null
+++ b/core/config/boolean.cc
@@ -0,0 +1,46 @@
+#include "core/pch.hh"
+
+#include "core/config/boolean.hh"
+
+config::Boolean::Boolean(bool default_value)
+{
+ m_value = default_value;
+}
+
+void config::Boolean::set(const char* value)
+{
+ m_value = from_string(value);
+}
+
+const char* config::Boolean::get(void) const
+{
+ return to_string(m_value);
+}
+
+bool config::Boolean::get_value(void) const
+{
+ return m_value;
+}
+
+void config::Boolean::set_value(bool value)
+{
+ m_value = value;
+}
+
+const char* config::Boolean::to_string(bool value)
+{
+ if(value) {
+ return "true";
+ } else {
+ return "false";
+ }
+}
+
+bool config::Boolean::from_string(const char* value)
+{
+ if(std::strcmp(value, "false") && !std::strcmp(value, "true")) {
+ return true;
+ } else {
+ return false;
+ }
+}
diff --git a/core/config/boolean.hh b/core/config/boolean.hh
new file mode 100644
index 0000000..5285676
--- /dev/null
+++ b/core/config/boolean.hh
@@ -0,0 +1,29 @@
+#ifndef CORE_CONFIG_BOOLEAN_HH
+#define CORE_CONFIG_BOOLEAN_HH 1
+#pragma once
+
+#include "core/config/ivalue.hh"
+
+namespace config
+{
+class Boolean : public IValue {
+public:
+ explicit Boolean(bool default_value = false);
+ virtual ~Boolean(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;
+
+public:
+ static const char* to_string(bool value);
+ static bool from_string(const char* value);
+};
+} // namespace config
+
+#endif // CORE_CONFIG_BOOLEAN_HH
diff --git a/core/config/ivalue.hh b/core/config/ivalue.hh
new file mode 100644
index 0000000..cf893a5
--- /dev/null
+++ b/core/config/ivalue.hh
@@ -0,0 +1,15 @@
+#ifndef CORE_CONFIG_IVALUE_HH
+#define CORE_CONFIG_IVALUE_HH 1
+#pragma once
+
+namespace config
+{
+class IValue {
+public:
+ virtual ~IValue(void) = default;
+ virtual void set(const char* value) = 0;
+ virtual const char* get(void) const = 0;
+};
+} // namespace config
+
+#endif // CORE_CONFIG_IVALUE_HH
diff --git a/core/config/number.hh b/core/config/number.hh
new file mode 100644
index 0000000..9907993
--- /dev/null
+++ b/core/config/number.hh
@@ -0,0 +1,129 @@
+#ifndef CORE_CONFIG_NUMBER_HH
+#define CORE_CONFIG_NUMBER_HH 1
+#pragma once
+
+#include "core/config/ivalue.hh"
+#include "core/math/concepts.hh"
+
+namespace config
+{
+template<math::Arithmetic T>
+class Number : public IValue {
+public:
+ explicit Number(T default_value = T(0));
+ explicit Number(T default_value, T min_value, T max_value);
+ virtual ~Number(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;
+};
+} // namespace config
+
+namespace config
+{
+class Int final : public Number<int> {
+public:
+ using Number<int>::Number;
+};
+
+class Float final : public Number<float> {
+public:
+ using Number<float>::Number;
+};
+
+class Unsigned final : public Number<unsigned int> {
+public:
+ using Number<unsigned int>::Number;
+};
+
+class Unsigned64 final : public Number<std::uint64_t> {
+public:
+ using Number<std::uint64_t>::Number;
+};
+
+class SizeType final : public Number<std::size_t> {
+public:
+ using Number<std::size_t>::Number;
+};
+} // namespace config
+
+template<math::Arithmetic T>
+inline config::Number<T>::Number(T default_value)
+{
+ m_value = default_value;
+ m_min_value = std::numeric_limits<T>::lowest();
+ m_max_value = std::numeric_limits<T>::max();
+ m_string = std::to_string(default_value);
+}
+
+template<math::Arithmetic T>
+inline config::Number<T>::Number(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<math::Arithmetic T>
+inline void config::Number<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<math::Arithmetic T>
+inline const char* config::Number<T>::get(void) const
+{
+ return m_string.c_str();
+}
+
+template<math::Arithmetic T>
+inline T config::Number<T>::get_value(void) const
+{
+ return m_value;
+}
+
+template<math::Arithmetic T>
+inline void config::Number<T>::set_value(T value)
+{
+ m_value = std::clamp(value, m_min_value, m_max_value);
+ m_string = std::to_string(m_value);
+}
+
+template<math::Arithmetic T>
+inline T config::Number<T>::get_min_value(void) const
+{
+ return m_min_value;
+}
+
+template<math::Arithmetic T>
+inline T config::Number<T>::get_max_value(void) const
+{
+ return m_max_value;
+}
+
+template<math::Arithmetic T>
+inline void config::Number<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_NUMBER_HH
diff --git a/core/config/string.cc b/core/config/string.cc
new file mode 100644
index 0000000..08e8c0d
--- /dev/null
+++ b/core/config/string.cc
@@ -0,0 +1,18 @@
+#include "core/pch.hh"
+
+#include "core/config/string.hh"
+
+config::String::String(const char* default_value)
+{
+ m_value = default_value;
+}
+
+void config::String::set(const char* value)
+{
+ m_value = value;
+}
+
+const char* config::String::get(void) const
+{
+ return m_value.c_str();
+}
diff --git a/core/config/string.hh b/core/config/string.hh
new file mode 100644
index 0000000..09d14c9
--- /dev/null
+++ b/core/config/string.hh
@@ -0,0 +1,22 @@
+#ifndef CORE_CONFIG_STRING_HH
+#define CORE_CONFIG_STRING_HH 1
+#pragma once
+
+#include "core/config/ivalue.hh"
+
+namespace config
+{
+class String : public IValue {
+public:
+ explicit String(const char* default_value);
+ virtual ~String(void) = default;
+
+ virtual void set(const char* value) override;
+ virtual const char* get(void) const override;
+
+private:
+ std::string m_value;
+};
+} // namespace config
+
+#endif // CORE_CONFIG_STRING_HH
diff --git a/core/io/CMakeLists.txt b/core/io/CMakeLists.txt
new file mode 100644
index 0000000..0639d2c
--- /dev/null
+++ b/core/io/CMakeLists.txt
@@ -0,0 +1,7 @@
+target_sources(core PRIVATE
+ "${CMAKE_CURRENT_LIST_DIR}/buffer.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/buffer.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/cmdline.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/cmdline.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/config_map.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/config_map.hh")
diff --git a/core/io/buffer.cc b/core/io/buffer.cc
new file mode 100644
index 0000000..62f981c
--- /dev/null
+++ b/core/io/buffer.cc
@@ -0,0 +1,208 @@
+#include "core/pch.hh"
+
+#include "core/io/buffer.hh"
+
+#include "core/math/constexpr.hh"
+
+io::ReadBuffer::ReadBuffer(const void* data, std::size_t size)
+{
+ reset(data, size);
+}
+
+io::ReadBuffer::ReadBuffer(const ENetPacket* packet)
+{
+ reset(packet);
+}
+
+io::ReadBuffer::ReadBuffer(PHYSFS_File* file)
+{
+ reset(file);
+}
+
+std::size_t io::ReadBuffer::size(void) const
+{
+ return m_vector.size();
+}
+
+const std::byte* io::ReadBuffer::data(void) const
+{
+ return m_vector.data();
+}
+
+void io::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 io::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 io::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 io::ReadBuffer::read_FP32(void)
+{
+ return math::uint32_to_float(read_UI32());
+}
+
+std::uint8_t io::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 io::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 io::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 io::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 io::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 io::WriteBuffer::size(void) const
+{
+ return m_vector.size();
+}
+
+const std::byte* io::WriteBuffer::data(void) const
+{
+ return m_vector.data();
+}
+
+void io::WriteBuffer::reset(void)
+{
+ m_vector.clear();
+}
+
+void io::WriteBuffer::write_UI8(std::uint8_t value)
+{
+ m_vector.push_back(static_cast<std::byte>(value));
+}
+
+void io::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 io::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 io::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 io::WriteBuffer::write_string(const std::string& value)
+{
+ const std::size_t size = math::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++]))) {
+ // empty
+ }
+}
+
+PHYSFS_File* io::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* io::WriteBuffer::to_packet(enet_uint32 flags) const
+{
+ return enet_packet_create(m_vector.data(), m_vector.size(), flags);
+}
diff --git a/core/io/buffer.hh b/core/io/buffer.hh
new file mode 100644
index 0000000..205381f
--- /dev/null
+++ b/core/io/buffer.hh
@@ -0,0 +1,260 @@
+#ifndef CORE_IO_BUFFER_HH
+#define CORE_IO_BUFFER_HH 1
+
+#include "core/math/floathacks.hh"
+
+namespace io
+{
+class ReadBuffer final {
+public:
+ 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;
+};
+} // namespace io
+
+namespace io
+{
+class WriteBuffer final {
+public:
+ 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;
+};
+} // namespace io
+
+inline std::int8_t io::ReadBuffer::read_I8(void)
+{
+ return static_cast<std::int8_t>(read_UI8());
+}
+
+inline std::int16_t io::ReadBuffer::read_I16(void)
+{
+ return static_cast<std::int16_t>(read_UI16());
+}
+
+inline std::int32_t io::ReadBuffer::read_I32(void)
+{
+ return static_cast<std::int32_t>(read_UI32());
+}
+
+inline std::int64_t io::ReadBuffer::read_I64(void)
+{
+ return static_cast<std::int64_t>(read_UI64());
+}
+
+inline io::ReadBuffer& io::ReadBuffer::operator>>(float& value)
+{
+ value = read_FP32();
+ return *this;
+}
+
+inline io::ReadBuffer& io::ReadBuffer::operator>>(std::int8_t& value)
+{
+ value = read_I8();
+ return *this;
+}
+
+inline io::ReadBuffer& io::ReadBuffer::operator>>(std::int16_t& value)
+{
+ value = read_I16();
+ return *this;
+}
+
+inline io::ReadBuffer& io::ReadBuffer::operator>>(std::int32_t& value)
+{
+ value = read_I32();
+ return *this;
+}
+
+inline io::ReadBuffer& io::ReadBuffer::operator>>(std::int64_t& value)
+{
+ value = read_I64();
+ return *this;
+}
+
+inline io::ReadBuffer& io::ReadBuffer::operator>>(std::uint8_t& value)
+{
+ value = read_UI8();
+ return *this;
+}
+
+inline io::ReadBuffer& io::ReadBuffer::operator>>(std::uint16_t& value)
+{
+ value = read_UI16();
+ return *this;
+}
+
+inline io::ReadBuffer& io::ReadBuffer::operator>>(std::uint32_t& value)
+{
+ value = read_UI32();
+ return *this;
+}
+
+inline io::ReadBuffer& io::ReadBuffer::operator>>(std::uint64_t& value)
+{
+ value = read_UI64();
+ return *this;
+}
+
+inline io::ReadBuffer& io::ReadBuffer::operator>>(std::string& value)
+{
+ value = read_string();
+ return *this;
+}
+
+inline void io::WriteBuffer::write_FP32(float value)
+{
+ write_UI32(math::float_to_uint32(value));
+}
+
+inline void io::WriteBuffer::write_I8(std::int8_t value)
+{
+ write_UI8(static_cast<std::uint8_t>(value));
+}
+
+inline void io::WriteBuffer::write_I16(std::int16_t value)
+{
+ write_UI16(static_cast<std::uint16_t>(value));
+}
+
+inline void io::WriteBuffer::write_I32(std::int32_t value)
+{
+ write_UI32(static_cast<std::uint32_t>(value));
+}
+
+inline void io::WriteBuffer::write_I64(std::int64_t value)
+{
+ write_UI64(static_cast<std::uint64_t>(value));
+}
+
+inline io::WriteBuffer& io::WriteBuffer::operator<<(float value)
+{
+ write_FP32(value);
+ return *this;
+}
+
+inline io::WriteBuffer& io::WriteBuffer::operator<<(std::int8_t value)
+{
+ write_I8(value);
+ return *this;
+}
+
+inline io::WriteBuffer& io::WriteBuffer::operator<<(std::int16_t value)
+{
+ write_I16(value);
+ return *this;
+}
+
+inline io::WriteBuffer& io::WriteBuffer::operator<<(std::int32_t value)
+{
+ write_I32(value);
+ return *this;
+}
+
+inline io::WriteBuffer& io::WriteBuffer::operator<<(std::int64_t value)
+{
+ write_I64(value);
+ return *this;
+}
+
+inline io::WriteBuffer& io::WriteBuffer::operator<<(std::uint8_t value)
+{
+ write_UI8(value);
+ return *this;
+}
+
+inline io::WriteBuffer& io::WriteBuffer::operator<<(std::uint16_t value)
+{
+ write_UI16(value);
+ return *this;
+}
+
+inline io::WriteBuffer& io::WriteBuffer::operator<<(std::uint32_t value)
+{
+ write_UI32(value);
+ return *this;
+}
+
+inline io::WriteBuffer& io::WriteBuffer::operator<<(std::uint64_t value)
+{
+ write_UI64(value);
+ return *this;
+}
+
+inline io::WriteBuffer& io::WriteBuffer::operator<<(const std::string& value)
+{
+ write_string(value);
+ return *this;
+}
+
+#endif // CORE_BUFFER_HH
diff --git a/core/io/cmdline.cc b/core/io/cmdline.cc
new file mode 100644
index 0000000..c43727d
--- /dev/null
+++ b/core/io/cmdline.cc
@@ -0,0 +1,83 @@
+#include "core/pch.hh"
+
+#include "core/io/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) {
+ // empty
+ }
+
+ return std::string(string.cbegin() + i, string.cend());
+}
+
+void io::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 io::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* io::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 io::cmdline::contains(const char* option)
+{
+ return options.count(option);
+}
diff --git a/core/io/cmdline.hh b/core/io/cmdline.hh
new file mode 100644
index 0000000..d68d9ef
--- /dev/null
+++ b/core/io/cmdline.hh
@@ -0,0 +1,13 @@
+#ifndef CORE_IO_CMDLINE_HH
+#define CORE_IO_CMDLINE_HH 1
+#pragma once
+
+namespace io::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 io::cmdline
+
+#endif // CORE_IO_CMDLINE_HH
diff --git a/core/io/config_map.cc b/core/io/config_map.cc
new file mode 100644
index 0000000..7bce1a6
--- /dev/null
+++ b/core/io/config_map.cc
@@ -0,0 +1,131 @@
+#include "core/pch.hh"
+
+#include "core/io/config_map.hh"
+
+#include "core/config/ivalue.hh"
+#include "core/io/cmdline.hh"
+#include "core/utils/string.hh"
+#include "core/version.hh"
+
+void io::ConfigMap::load_cmdline(void)
+{
+ for(auto it : m_values) {
+ if(auto value = io::cmdline::get(it.first.c_str())) {
+ it.second->set(value);
+ }
+ }
+}
+
+bool io::ConfigMap::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 = utils::trim_whitespace(line);
+ } else {
+ kv_string = utils::trim_whitespace(line.substr(0, comment));
+ }
+
+ if(utils::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 = utils::trim_whitespace(kv_string.substr(0, separator));
+ auto kv_value = utils::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 io::ConfigMap::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 io::ConfigMap::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* io::ConfigMap::get_value(const char* name) const
+{
+ auto kv_pair = m_values.find(name);
+ if(kv_pair != m_values.cend()) {
+ return kv_pair->second->get();
+ } else {
+ return nullptr;
+ }
+}
+
+void io::ConfigMap::add_value(const char* name, config::IValue& vref)
+{
+ m_values.insert_or_assign(name, &vref);
+}
+
+const config::IValue* io::ConfigMap::find(const char* name) const
+{
+ auto kv_pair = m_values.find(name);
+
+ if(kv_pair != m_values.cend()) {
+ return kv_pair->second;
+ } else {
+ return nullptr;
+ }
+}
diff --git a/core/io/config_map.hh b/core/io/config_map.hh
new file mode 100644
index 0000000..8ff92e1
--- /dev/null
+++ b/core/io/config_map.hh
@@ -0,0 +1,33 @@
+#ifndef CORE_CONFIG_MAP_HH
+#define CORE_CONFIG_MAP_HH 1
+#pragma once
+
+namespace config
+{
+class IValue;
+} // namespace config
+
+namespace io
+{
+class ConfigMap final {
+public:
+ ConfigMap(void) = default;
+ virtual ~ConfigMap(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, config::IValue& vref);
+
+ const config::IValue* find(const char* name) const;
+
+private:
+ std::unordered_map<std::string, config::IValue*> m_values;
+};
+} // namespace io
+
+#endif // CORE_CONFIG_MAP_HH
diff --git a/core/math/CMakeLists.txt b/core/math/CMakeLists.txt
new file mode 100644
index 0000000..ab03087
--- /dev/null
+++ b/core/math/CMakeLists.txt
@@ -0,0 +1,11 @@
+target_sources(core PRIVATE
+ "${CMAKE_CURRENT_LIST_DIR}/aabb.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/aabb.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/angles.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/concepts.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/constexpr.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/crc64.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/crc64.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/floathacks.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/randomizer.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/vectors.hh")
diff --git a/core/math/aabb.cc b/core/math/aabb.cc
new file mode 100644
index 0000000..f5c7e14
--- /dev/null
+++ b/core/math/aabb.cc
@@ -0,0 +1,59 @@
+#include "core/pch.hh"
+
+#include "core/math/aabb.hh"
+
+math::AABB::AABB(const glm::fvec3& min, const glm::fvec3& max)
+{
+ set_bounds(min, max);
+}
+
+void math::AABB::set_bounds(const glm::fvec3& min, const glm::fvec3& max)
+{
+ this->min = min;
+ this->max = max;
+}
+
+void math::AABB::set_offset(const glm::fvec3& base, const glm::fvec3& size)
+{
+ this->min = base;
+ this->max = base + size;
+}
+
+bool math::AABB::contains(const glm::fvec3& point) const
+{
+ auto result = true;
+ result = result && (point.x >= min.x) && (point.x <= max.x);
+ result = result && (point.y >= min.y) && (point.y <= max.y);
+ result = result && (point.z >= min.z) && (point.z <= max.z);
+ return result;
+}
+
+bool math::AABB::intersect(const AABB& other_box) const
+{
+ auto result = true;
+ result = result && (min.x < other_box.max.x) && (max.x > other_box.min.x);
+ result = result && (min.y < other_box.max.y) && (max.y > other_box.min.y);
+ result = result && (min.z < other_box.max.z) && (max.z > other_box.min.z);
+ return result;
+}
+
+math::AABB math::AABB::combine_with(const math::AABB& other_box) const
+{
+ AABB result;
+ result.set_bounds(min, other_box.max);
+ return result;
+}
+
+math::AABB math::AABB::multiply_with(const math::AABB& other_box) const
+{
+ AABB result;
+ result.set_bounds(other_box.min, max);
+ return result;
+}
+
+math::AABB math::AABB::push(const glm::fvec3& vector) const
+{
+ AABB result;
+ result.set_bounds(min + vector, max + vector);
+ return result;
+}
diff --git a/core/math/aabb.hh b/core/math/aabb.hh
new file mode 100644
index 0000000..943eeee
--- /dev/null
+++ b/core/math/aabb.hh
@@ -0,0 +1,32 @@
+#ifndef CORE_MATH_AABB_HH
+#define CORE_MATH_AABB_HH 1
+#pragma once
+
+namespace math
+{
+class AABB final {
+public:
+ AABB(void) = default;
+ 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;
+};
+} // namespace math
+
+#endif // CORE_MATH_AABB_HH
diff --git a/core/math/angles.hh b/core/math/angles.hh
new file mode 100644
index 0000000..868160b
--- /dev/null
+++ b/core/math/angles.hh
@@ -0,0 +1,107 @@
+#ifndef CORE_MATH_ANGLES_HH
+#define CORE_MATH_ANGLES_HH 1
+#pragma once
+
+#include "core/math/constexpr.hh"
+
+constexpr float A180 = math::radians(180.0f);
+constexpr float A360 = math::radians(360.0f);
+
+namespace math
+{
+float wrap_180(float angle);
+float wrap_360(float angle);
+} // namespace math
+
+namespace math
+{
+glm::fvec3 wrap_180(const glm::fvec3& angles);
+glm::fvec3 wrap_360(const glm::fvec3& angles);
+} // namespace math
+
+namespace math
+{
+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 math
+
+inline float math::wrap_180(float angle)
+{
+ const auto result = std::fmod(angle + A180, A360);
+
+ if(result < 0.0f) {
+ return result + A180;
+ }
+
+ return result - A180;
+}
+
+inline float math::wrap_360(float angle)
+{
+ return std::fmod(std::fmod(angle, A360) + A360, A360);
+}
+
+inline glm::fvec3 math::wrap_180(const glm::fvec3& angles)
+{
+ return glm::fvec3 {
+ math::wrap_180(angles.x),
+ math::wrap_180(angles.y),
+ math::wrap_180(angles.z),
+ };
+}
+
+inline glm::fvec3 math::wrap_360(const glm::fvec3& angles)
+{
+ return glm::fvec3 {
+ math::wrap_360(angles.x),
+ math::wrap_360(angles.y),
+ math::wrap_360(angles.z),
+ };
+}
+
+inline void math::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 math::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_MATH_ANGLES_HH
diff --git a/core/math/concepts.hh b/core/math/concepts.hh
new file mode 100644
index 0000000..1b9beb9
--- /dev/null
+++ b/core/math/concepts.hh
@@ -0,0 +1,15 @@
+#ifndef CORE_MATH_CONCEPTS_HH
+#define CORE_MATH_CONCEPTS_HH 1
+#pragma once
+
+namespace math
+{
+template<typename T>
+concept Arithmetic = std::is_arithmetic_v<T>;
+template<typename T>
+concept Integer = std::is_integral_v<T>;
+template<typename T>
+concept FloatingPoint = std::is_floating_point_v<T>;
+} // namespace math
+
+#endif // CORE_MATH_CONCEPTS_HH
diff --git a/core/math/constexpr.hh b/core/math/constexpr.hh
new file mode 100644
index 0000000..3e4fcfa
--- /dev/null
+++ b/core/math/constexpr.hh
@@ -0,0 +1,191 @@
+#ifndef CORE_MATH_CONSTEXPR_HH
+#define CORE_MATH_CONSTEXPR_HH 1
+#pragma once
+
+#include "core/math/concepts.hh"
+
+namespace math
+{
+template<math::Arithmetic 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<math::Integer T, math::FloatingPoint F>
+constexpr static inline const T ceil(const F x);
+template<math::Arithmetic T>
+constexpr static inline const T degrees(const T x);
+template<math::Integer T, math::FloatingPoint F>
+constexpr static inline const T floor(const F x);
+template<math::Arithmetic T>
+constexpr static inline const T clamp(const T x, const T min, const T max);
+template<math::Arithmetic T, math::FloatingPoint F>
+constexpr static inline const T lerp(const T x, const T y, const F a);
+template<math::Arithmetic T>
+constexpr static inline const T log2(const T x);
+template<math::Arithmetic T>
+constexpr static inline const T max(const T x, const T y);
+template<math::Arithmetic T>
+constexpr static inline const T min(const T x, const T y);
+template<math::Integer T>
+requires std::is_signed_v<T>
+constexpr static inline const T mod_signed(const T x, const T m);
+template<math::Arithmetic T>
+constexpr static inline const T pow2(const T x);
+template<math::Arithmetic T>
+constexpr static inline const T radians(const T x);
+template<math::Arithmetic T>
+constexpr static inline const bool range(const T x, const T min, const T max);
+template<math::Arithmetic T, math::FloatingPoint F>
+constexpr static inline const T sign(const F x);
+template<math::Arithmetic T, math::FloatingPoint F>
+constexpr static inline const T smoothstep(const T x, const T y, const F a);
+} // namespace math
+
+template<math::Arithmetic T>
+constexpr static inline const T math::abs(const T x)
+{
+ if(x < static_cast<T>(0)) {
+ return -x;
+ } else {
+ return x;
+ }
+}
+
+template<typename T, std::size_t L>
+constexpr static inline const std::size_t math::array_size(const T (&)[L])
+{
+ return L;
+}
+
+template<math::Integer T, math::FloatingPoint F>
+constexpr static inline const T math::ceil(const F x)
+{
+ const T ival = static_cast<T>(x);
+
+ if(ival < x) {
+ return ival + static_cast<T>(1);
+ } else {
+ return ival;
+ }
+}
+
+template<math::Arithmetic T>
+constexpr static inline const T math::degrees(const T x)
+{
+ return x * static_cast<T>(180.0) / static_cast<T>(M_PI);
+}
+
+template<math::Integer T, math::FloatingPoint F>
+constexpr static inline const T math::floor(const F x)
+{
+ const T ival = static_cast<T>(x);
+
+ if(ival > x) {
+ return ival - static_cast<T>(1);
+ } else {
+ return ival;
+ }
+}
+
+template<math::Arithmetic T>
+constexpr static inline const T math::clamp(const T x, const T min, const T max)
+{
+ if(x < min) {
+ return min;
+ } else if(x > max) {
+ return max;
+ } else {
+ return x;
+ }
+}
+
+template<math::Arithmetic T, math::FloatingPoint F>
+constexpr static inline const T math::lerp(const T x, const T y, const F a)
+{
+ return static_cast<T>(static_cast<F>(x) * (static_cast<F>(1.0f) - a) + static_cast<F>(y) * a);
+}
+
+template<math::Arithmetic T>
+constexpr static inline const T math::log2(const T x)
+{
+ if(x < 2) {
+ return 0;
+ } else {
+ return math::log2<T>((x + 1) >> 1) + 1;
+ }
+}
+
+template<math::Arithmetic T>
+constexpr static inline const T math::max(const T x, const T y)
+{
+ if(x < y) {
+ return y;
+ } else {
+ return x;
+ }
+}
+
+template<math::Arithmetic T>
+constexpr static inline const T math::min(const T x, const T y)
+{
+ if(x > y) {
+ return y;
+ } else {
+ return x;
+ }
+}
+
+template<math::Integer T>
+requires std::is_signed_v<T>
+constexpr static inline const T math::mod_signed(const T x, const T m)
+{
+ auto result = static_cast<T>(x % m);
+
+ if(result < T(0)) {
+ return result + m;
+ } else {
+ return result;
+ }
+}
+
+template<math::Arithmetic T>
+constexpr static inline const T math::pow2(const T x)
+{
+ T value = static_cast<T>(1);
+ while(value < x)
+ value *= static_cast<T>(2);
+ return value;
+}
+
+template<math::Arithmetic T>
+constexpr static inline const T math::radians(const T x)
+{
+ return x * static_cast<T>(M_PI) / static_cast<T>(180.0);
+}
+
+template<math::Arithmetic T>
+constexpr static inline const bool math::range(const T x, const T min, const T max)
+{
+ return ((x >= min) && (x <= max));
+}
+
+template<math::Arithmetic T, math::FloatingPoint F>
+constexpr static inline const T math::sign(const F x)
+{
+ if(x < F(0)) {
+ return T(-1);
+ } else if(x > F(0)) {
+ return T(+1);
+ } else {
+ return T(0);
+ }
+}
+
+template<math::Arithmetic T, math::FloatingPoint F>
+constexpr static inline const T math::smoothstep(const T x, const T y, const F a)
+{
+ const F t = math::clamp<F>((a - x) / (y - x), F(0), F(1));
+ return static_cast<T>(t * t * (F(3) - F(2) * t));
+}
+
+#endif // CORE_MATH_CONSTEXPR_HH
diff --git a/core/math/crc64.cc b/core/math/crc64.cc
new file mode 100644
index 0000000..15b67cb
--- /dev/null
+++ b/core/math/crc64.cc
@@ -0,0 +1,286 @@
+#include "core/pch.hh"
+
+#include "core/math/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
+constexpr 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 math::crc64(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 math::crc64(const std::vector<std::byte>& buffer, std::uint64_t combine)
+{
+ return math::crc64(buffer.data(), buffer.size(), combine);
+}
+
+std::uint64_t math::crc64(const std::string& buffer, std::uint64_t combine)
+{
+ return math::crc64(buffer.data(), buffer.size(), combine);
+}
diff --git a/core/math/crc64.hh b/core/math/crc64.hh
new file mode 100644
index 0000000..ac2c4b6
--- /dev/null
+++ b/core/math/crc64.hh
@@ -0,0 +1,12 @@
+#ifndef CORE_MATH_CRC64_HH
+#define CORE_MATH_CRC64_HH 1
+#pragma once
+
+namespace math
+{
+std::uint64_t crc64(const void* buffer, std::size_t size, std::uint64_t combine = UINT64_C(0));
+std::uint64_t crc64(const std::vector<std::byte>& buffer, std::uint64_t combine = UINT64_C(0));
+std::uint64_t crc64(const std::string& buffer, std::uint64_t combine = UINT64_C(0));
+} // namespace math
+
+#endif // CORE_MATH_CRC64_HH
diff --git a/core/math/floathacks.hh b/core/math/floathacks.hh
new file mode 100644
index 0000000..dc8c235
--- /dev/null
+++ b/core/math/floathacks.hh
@@ -0,0 +1,56 @@
+#ifndef CORE_MATH_FLOATHACKS_HH
+#define CORE_MATH_FLOATHACKS_HH 1
+#pragma once
+
+namespace math
+{
+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 math
+
+static_assert(std::numeric_limits<float>::is_iec559, "Floathacks only works with IEEE 754 compliant floats");
+static_assert(sizeof(std::int32_t) == sizeof(float), "Floathacks requires 32-bit integers to match float size");
+
+static inline float math::int32_to_float(const std::int32_t value)
+{
+ union {
+ std::int32_t src;
+ float dst;
+ } hack;
+ hack.src = value;
+ return hack.dst;
+}
+
+static inline float math::uint32_to_float(const std::uint32_t value)
+{
+ union {
+ std::uint32_t src;
+ float dst;
+ } hack;
+ hack.src = value;
+ return hack.dst;
+}
+
+static inline std::int32_t math::float_to_int32(const float value)
+{
+ union {
+ float src;
+ std::int32_t dst;
+ } hack;
+ hack.src = value;
+ return hack.dst;
+}
+
+static inline std::uint32_t math::float_to_uint32(const float value)
+{
+ union {
+ float src;
+ std::uint32_t dst;
+ } hack;
+ hack.src = value;
+ return hack.dst;
+}
+
+#endif // CORE_MATH_FLOATHACKS_HH
diff --git a/core/math/randomizer.hh b/core/math/randomizer.hh
new file mode 100644
index 0000000..206af42
--- /dev/null
+++ b/core/math/randomizer.hh
@@ -0,0 +1,60 @@
+#ifndef CORE_MATH_RANDOMIZER_HH
+#define CORE_MATH_RANDOMIZER_HH 1
+#pragma once
+
+namespace math
+{
+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;
+};
+} // namespace math
+
+template<typename T>
+inline math::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 math::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 math::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& math::Randomizer<T>::get(void)
+{
+ return m_vector.at(m_dist(m_twister));
+}
+
+template<typename T>
+inline void math::Randomizer<T>::clear(void)
+{
+ m_vector.clear();
+ m_dist = std::uniform_int_distribution<std::size_t>(0, 0);
+}
+
+#endif // CORE_MATH_RANDOMIZER_HH
diff --git a/core/math/vectors.hh b/core/math/vectors.hh
new file mode 100644
index 0000000..06d1bcf
--- /dev/null
+++ b/core/math/vectors.hh
@@ -0,0 +1,47 @@
+#ifndef CORE_MATH_VECTORS_HH
+#define CORE_MATH_VECTORS_HH 1
+#pragma once
+
+#include "core/math/concepts.hh"
+
+// core/vectors.hh - because NO ONE would POSSIBLY
+// need integer-based distance calculations in a
+// game about voxels. That would be INSANE! :D
+
+namespace math
+{
+template<math::Arithmetic T>
+constexpr static inline const T length2(const glm::vec<2, T>& vector);
+template<math::Arithmetic T>
+constexpr static inline const T length2(const glm::vec<3, T>& vector);
+template<math::Arithmetic T>
+constexpr static inline const T distance2(const glm::vec<2, T>& vector_a, const glm::vec<2, T>& vector_b);
+template<math::Arithmetic T>
+constexpr static inline const T distance2(const glm::vec<3, T>& vector_a, const glm::vec<3, T>& vector_b);
+} // namespace math
+
+template<math::Arithmetic T>
+constexpr static inline const T math::length2(const glm::vec<2, T>& vector)
+{
+ return (vector.x * vector.x) + (vector.y * vector.y);
+}
+
+template<math::Arithmetic T>
+constexpr static inline const T math::length2(const glm::vec<3, T>& vector)
+{
+ return (vector.x * vector.x) + (vector.y * vector.y) + (vector.z * vector.z);
+}
+
+template<math::Arithmetic T>
+constexpr static inline const T math::distance2(const glm::vec<2, T>& vector_a, const glm::vec<2, T>& vector_b)
+{
+ return math::length2(vector_a - vector_b);
+}
+
+template<math::Arithmetic T>
+constexpr static inline const T math::distance2(const glm::vec<3, T>& vector_a, const glm::vec<3, T>& vector_b)
+{
+ return math::length2(vector_a - vector_b);
+}
+
+#endif // CORE_MATH_VECTORS_HH
diff --git a/core/pch.hh b/core/pch.hh
new file mode 100644
index 0000000..924b1dd
--- /dev/null
+++ b/core/pch.hh
@@ -0,0 +1,50 @@
+#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 <concepts>
+#include <filesystem>
+#include <format>
+#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/matrix_transform.hpp>
+#include <glm/gtc/quaternion.hpp>
+#include <glm/gtc/type_ptr.hpp>
+
+#include <physfs.h>
+
+#include <spdlog/spdlog.h>
+
+#include <stb_image.h>
+#include <stb_image_write.h>
+
+#endif // CORE_PCH_HH
diff --git a/core/resource/CMakeLists.txt b/core/resource/CMakeLists.txt
new file mode 100644
index 0000000..ef2ffae
--- /dev/null
+++ b/core/resource/CMakeLists.txt
@@ -0,0 +1,6 @@
+target_sources(core PRIVATE
+ "${CMAKE_CURRENT_LIST_DIR}/binfile.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/binfile.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/image.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/image.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/resource.hh")
diff --git a/core/resource/binfile.cc b/core/resource/binfile.cc
new file mode 100644
index 0000000..e17db50
--- /dev/null
+++ b/core/resource/binfile.cc
@@ -0,0 +1,70 @@
+#include "core/pch.hh"
+
+#include "core/resource/binfile.hh"
+
+#include "core/resource/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/resource/binfile.hh b/core/resource/binfile.hh
new file mode 100644
index 0000000..6bb5e3d
--- /dev/null
+++ b/core/resource/binfile.hh
@@ -0,0 +1,10 @@
+#ifndef CORE_RESOURCE_BINFILE_HH
+#define CORE_RESOURCE_BINFILE_HH 1
+#pragma once
+
+struct BinFile final {
+ std::byte* buffer;
+ std::size_t size;
+};
+
+#endif // CORE_RESOURCE_BINFILE_HH
diff --git a/core/resource/image.cc b/core/resource/image.cc
new file mode 100644
index 0000000..1601703
--- /dev/null
+++ b/core/resource/image.cc
@@ -0,0 +1,112 @@
+#include "core/pch.hh"
+
+#include "core/resource/image.hh"
+
+#include "core/resource/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/resource/image.hh b/core/resource/image.hh
new file mode 100644
index 0000000..13e912e
--- /dev/null
+++ b/core/resource/image.hh
@@ -0,0 +1,13 @@
+#ifndef CORE_RESOURCE_IMAGE_HH
+#define CORE_RESOURCE_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_RESOURCE_IMAGE_HH
diff --git a/core/resource/resource.hh b/core/resource/resource.hh
new file mode 100644
index 0000000..4946ffa
--- /dev/null
+++ b/core/resource/resource.hh
@@ -0,0 +1,18 @@
+#ifndef CORE_RESOURCE_RESOURCE_HH
+#define CORE_RESOURCE_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_RESOURCE_HH
diff --git a/core/utils/CMakeLists.txt b/core/utils/CMakeLists.txt
new file mode 100644
index 0000000..4f96261
--- /dev/null
+++ b/core/utils/CMakeLists.txt
@@ -0,0 +1,5 @@
+target_sources(core PRIVATE
+ "${CMAKE_CURRENT_LIST_DIR}/epoch.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/epoch.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/string.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/string.hh")
diff --git a/core/utils/epoch.cc b/core/utils/epoch.cc
new file mode 100644
index 0000000..36bdb79
--- /dev/null
+++ b/core/utils/epoch.cc
@@ -0,0 +1,39 @@
+#include "core/pch.hh"
+
+#include "core/utils/epoch.hh"
+
+std::uint64_t utils::unix_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 utils::unix_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 utils::unix_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 utils::signed_unix_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 utils::signed_unix_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 utils::signed_unix_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/utils/epoch.hh b/core/utils/epoch.hh
new file mode 100644
index 0000000..1cd7f43
--- /dev/null
+++ b/core/utils/epoch.hh
@@ -0,0 +1,19 @@
+#ifndef CORE_UTILS_EPOCH_HH
+#define CORE_UTILS_EPOCH_HH 1
+#pragma once
+
+namespace utils
+{
+std::uint64_t unix_seconds(void);
+std::uint64_t unix_milliseconds(void);
+std::uint64_t unix_microseconds(void);
+} // namespace utils
+
+namespace utils
+{
+std::int64_t signed_unix_seconds(void);
+std::int64_t signed_unix_milliseconds(void);
+std::int64_t signed_unix_microseconds(void);
+} // namespace utils
+
+#endif // CORE_UTILS_EPOCH_HH
diff --git a/core/utils/string.cc b/core/utils/string.cc
new file mode 100644
index 0000000..5497cec
--- /dev/null
+++ b/core/utils/string.cc
@@ -0,0 +1,54 @@
+#include "core/pch.hh"
+
+#include "core/utils/string.hh"
+
+constexpr static const char* WHITESPACE_CHARS = " \t\r\n";
+
+bool utils::is_whitespace(const std::string& string)
+{
+ if(string.find_first_not_of(WHITESPACE_CHARS) == std::string::npos) {
+ return true;
+ } else if((string.size() == 1) && string[0] == 0x00) {
+ return true;
+ } else {
+ return string.empty();
+ }
+}
+
+std::string utils::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> utils::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 utils::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();
+ } else {
+ return string.substr(su, sv - su + 1);
+ }
+}
diff --git a/core/utils/string.hh b/core/utils/string.hh
new file mode 100644
index 0000000..3ba56a0
--- /dev/null
+++ b/core/utils/string.hh
@@ -0,0 +1,21 @@
+#ifndef CORE_UTILS_STRING_HH
+#define CORE_UTILS_STRING_HH 1
+#pragma once
+
+namespace utils
+{
+bool is_whitespace(const std::string& string);
+} // namespace utils
+
+namespace utils
+{
+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 utils
+
+namespace utils
+{
+std::string trim_whitespace(const std::string& string);
+} // namespace utils
+
+#endif // CORE_UTILS_STRING_HH
diff --git a/core/version.cc b/core/version.cc
new file mode 100644
index 0000000..be19502
--- /dev/null
+++ b/core/version.cc
@@ -0,0 +1,12 @@
+#include "core/pch.hh"
+
+#include "core/version.hh"
+
+// clang-format off
+const unsigned long project_version_major = 0;
+const unsigned long project_version_minor = 0;
+const unsigned long project_version_patch = 1;
+const unsigned long project_version_tweak = 2526;
+// clang-format on
+
+const char* project_version_string = "0.0.1.2526";
diff --git a/core/version.cc.in b/core/version.cc.in
new file mode 100644
index 0000000..0c255c9
--- /dev/null
+++ b/core/version.cc.in
@@ -0,0 +1,12 @@
+#include "core/pch.hh"
+
+#include "core/version.hh"
+
+// clang-format off
+const unsigned long project_version_major = ${PROJECT_VERSION_MAJOR};
+const unsigned long project_version_minor = ${PROJECT_VERSION_MINOR};
+const unsigned long project_version_patch = ${PROJECT_VERSION_PATCH};
+const unsigned long project_version_tweak = ${PROJECT_VERSION_TWEAK};
+// clang-format on
+
+const char* project_version_string = "${PROJECT_VERSION}";
diff --git a/core/version.hh b/core/version.hh
new file mode 100644
index 0000000..627df17
--- /dev/null
+++ b/core/version.hh
@@ -0,0 +1,12 @@
+#ifndef CORE_VERSION_HH
+#define CORE_VERSION_HH 1
+#pragma once
+
+extern const unsigned long project_version_major;
+extern const unsigned long project_version_minor;
+extern const unsigned long project_version_patch;
+extern const unsigned long project_version_tweak;
+
+extern const char* project_version_string;
+
+#endif // CORE_VERSION_HH