aboutsummaryrefslogtreecommitdiff
path: root/app/source/Cplt/Utils
diff options
context:
space:
mode:
authorrtk0c <[email protected]>2022-06-30 21:38:53 -0700
committerrtk0c <[email protected]>2022-06-30 21:38:53 -0700
commit7fe47a9d5b1727a61dc724523b530762f6d6ba19 (patch)
treee95be6e66db504ed06d00b72c579565bab873277 /app/source/Cplt/Utils
parent2cf952088d375ac8b2f45b144462af0953436cff (diff)
Restructure project
Diffstat (limited to 'app/source/Cplt/Utils')
-rw-r--r--app/source/Cplt/Utils/Color.hpp202
-rw-r--r--app/source/Cplt/Utils/Hash.hpp15
-rw-r--r--app/source/Cplt/Utils/I18n.hpp10
-rw-r--r--app/source/Cplt/Utils/IO/Archive.cpp57
-rw-r--r--app/source/Cplt/Utils/IO/Archive.hpp18
-rw-r--r--app/source/Cplt/Utils/IO/CstdioFile.cpp36
-rw-r--r--app/source/Cplt/Utils/IO/CstdioFile.hpp17
-rw-r--r--app/source/Cplt/Utils/IO/DataStream.cpp283
-rw-r--r--app/source/Cplt/Utils/IO/DataStream.hpp210
-rw-r--r--app/source/Cplt/Utils/IO/FileStream.cpp7
-rw-r--r--app/source/Cplt/Utils/IO/FileStream.hpp97
-rw-r--r--app/source/Cplt/Utils/IO/FileStream_Cstdio.inl126
-rw-r--r--app/source/Cplt/Utils/IO/FileStream_Custom.inl358
-rw-r--r--app/source/Cplt/Utils/IO/Helper.hpp43
-rw-r--r--app/source/Cplt/Utils/IO/StringIntegration.hpp37
-rw-r--r--app/source/Cplt/Utils/IO/TslArrayIntegration.hpp50
-rw-r--r--app/source/Cplt/Utils/IO/TslRobinIntegration.hpp78
-rw-r--r--app/source/Cplt/Utils/IO/UuidIntegration.hpp27
-rw-r--r--app/source/Cplt/Utils/IO/VectorIntegration.hpp42
-rw-r--r--app/source/Cplt/Utils/IO/fwd.hpp13
-rw-r--r--app/source/Cplt/Utils/Macros.hpp13
-rw-r--r--app/source/Cplt/Utils/Math.hpp11
-rw-r--r--app/source/Cplt/Utils/RTTI.hpp49
-rw-r--r--app/source/Cplt/Utils/ScopeGuard.hpp39
-rw-r--r--app/source/Cplt/Utils/Sigslot.cpp233
-rw-r--r--app/source/Cplt/Utils/Sigslot.hpp165
-rw-r--r--app/source/Cplt/Utils/Size.hpp65
-rw-r--r--app/source/Cplt/Utils/StandardDirectories.cpp78
-rw-r--r--app/source/Cplt/Utils/StandardDirectories.hpp10
-rw-r--r--app/source/Cplt/Utils/Time.cpp29
-rw-r--r--app/source/Cplt/Utils/Time.hpp11
-rw-r--r--app/source/Cplt/Utils/UUID.hpp5
-rw-r--r--app/source/Cplt/Utils/Variant.hpp33
-rw-r--r--app/source/Cplt/Utils/Vector.hpp144
-rw-r--r--app/source/Cplt/Utils/VectorHash.hpp46
-rw-r--r--app/source/Cplt/Utils/fwd.hpp17
36 files changed, 2674 insertions, 0 deletions
diff --git a/app/source/Cplt/Utils/Color.hpp b/app/source/Cplt/Utils/Color.hpp
new file mode 100644
index 0000000..15fe6a1
--- /dev/null
+++ b/app/source/Cplt/Utils/Color.hpp
@@ -0,0 +1,202 @@
+#pragma once
+
+#include <Cplt/Utils/Math.hpp>
+#include <Cplt/Utils/Vector.hpp>
+#include <Cplt/Utils/fwd.hpp>
+
+#include <imgui.h>
+#include <algorithm>
+#include <cstdint>
+#include <limits>
+
+class RgbaColor
+{
+public:
+ uint8_t r;
+ uint8_t g;
+ uint8_t b;
+ uint8_t a;
+
+public:
+ constexpr RgbaColor() noexcept
+ : r{ 255 }
+ , g{ 255 }
+ , b{ 255 }
+ , a{ 255 }
+ {
+ }
+
+ constexpr RgbaColor(float r, float g, float b, float a = 1.0f) noexcept
+ : r{ static_cast<uint8_t>(r * 255.0f) }
+ , g{ static_cast<uint8_t>(g * 255.0f) }
+ , b{ static_cast<uint8_t>(b * 255.0f) }
+ , a{ static_cast<uint8_t>(a * 255.0f) }
+ {
+ }
+
+ constexpr RgbaColor(int r, int g, int b, int a = 255) noexcept
+ : r{ static_cast<uint8_t>(r & 0xFF) }
+ , g{ static_cast<uint8_t>(g & 0xFF) }
+ , b{ static_cast<uint8_t>(b & 0xFF) }
+ , a{ static_cast<uint8_t>(a & 0xFF) }
+ {
+ }
+
+ constexpr RgbaColor(uint32_t rgba) noexcept
+ : r{ static_cast<uint8_t>((rgba >> 0) & 0xFF) }
+ , g{ static_cast<uint8_t>((rgba >> 8) & 0xFF) }
+ , b{ static_cast<uint8_t>((rgba >> 16) & 0xFF) }
+ , a{ static_cast<uint8_t>((rgba >> 24) & 0xFF) }
+ {
+ }
+
+ constexpr uint32_t GetScalar() const noexcept
+ {
+ uint32_t res = 0;
+ res |= r << 24;
+ res |= g << 16;
+ res |= b << 8;
+ res |= a;
+ return res;
+ }
+
+ constexpr void SetScalar(uint32_t scalar) noexcept
+ {
+ r = (scalar >> 0) & 0xFF;
+ g = (scalar >> 8) & 0xFF;
+ b = (scalar >> 16) & 0xFF;
+ a = (scalar >> 24) & 0xFF;
+ }
+
+ constexpr float GetNormalizedRed() const noexcept
+ {
+ return r / 255.0f;
+ }
+
+ constexpr float GetNormalizedGreen() const noexcept
+ {
+ return g / 255.0f;
+ }
+
+ constexpr float GetNormalizedBlue() const noexcept
+ {
+ return b / 255.0f;
+ }
+
+ constexpr float GetNormalizedAlpha() const noexcept
+ {
+ return a / 255.0f;
+ }
+
+ constexpr Vec4i AsVec4i() const noexcept
+ {
+ return Vec4i{ r, g, b, a };
+ }
+
+ constexpr Vec4f AsVec4f() const noexcept
+ {
+ return Vec4f{
+ GetNormalizedRed(),
+ GetNormalizedGreen(),
+ GetNormalizedBlue(),
+ GetNormalizedAlpha(),
+ };
+ }
+
+ ImVec4 AsImVec() const
+ {
+ auto v = AsVec4f();
+ return ImVec4{ v.x, v.y, v.z, v.w };
+ }
+
+ ImColor AsImColor() const
+ {
+ auto v = AsVec4f();
+ return ImColor{ v.x, v.y, v.z, v.w };
+ }
+
+ ImU32 AsImU32() const
+ {
+ ImU32 res;
+ res |= r << IM_COL32_R_SHIFT;
+ res |= g << IM_COL32_G_SHIFT;
+ res |= b << IM_COL32_B_SHIFT;
+ res |= a << IM_COL32_A_SHIFT;
+ return res;
+ }
+
+ constexpr void SetVec(const Vec4f& vec) noexcept
+ {
+ r = (uint8_t)(vec.x * 255.0f);
+ g = (uint8_t)(vec.y * 255.0f);
+ b = (uint8_t)(vec.z * 255.0f);
+ a = (uint8_t)(vec.w * 255.0f);
+ }
+
+ // Forward declaring because cyclic reference between RgbaColor and HsvColor
+ constexpr HsvColor ToHsv() const noexcept;
+
+ friend constexpr bool operator==(const RgbaColor&, const RgbaColor&) noexcept = default;
+};
+
+class HsvColor
+{
+public:
+ float h;
+ float s;
+ float v;
+ float a;
+
+public:
+ constexpr HsvColor() noexcept
+ : h{ 0.0f }
+ , s{ 0.0f }
+ , v{ 1.0f }
+ , a{ 1.0f }
+ {
+ }
+
+ constexpr HsvColor(float h, float s, float v, float a) noexcept
+ : h{ h }
+ , s{ s }
+ , v{ v }
+ , a{ a }
+ {
+ }
+
+ // Forward declaring because cyclic reference between RgbaColor and HsvColor
+ constexpr RgbaColor ToRgba() const noexcept;
+};
+
+constexpr HsvColor RgbaColor::ToHsv() const noexcept
+{
+ float fr = GetNormalizedRed();
+ float fg = GetNormalizedBlue();
+ float fb = GetNormalizedGreen();
+ float fa = GetNormalizedAlpha();
+
+ auto p = fg < fb ? Vec4f{ fb, fg, -1, 2.0f / 3.0f } : Vec4f{ fg, fb, 0, -1.0f / 3.0f };
+ auto q = fr < p.x ? Vec4f{ p.x, p.y, p.w, fr } : Vec4f{ fr, p.y, p.z, p.x };
+ float c = q.x - std::min(q.w, q.y);
+ float h = MathUtils::Abs((q.w - q.y) / (6 * c + std::numeric_limits<float>::epsilon()) + q.z);
+
+ Vec3f hcv{ h, c, q.x };
+ float s = hcv.y / (hcv.z + std::numeric_limits<float>::epsilon());
+ return HsvColor(hcv.x, s, hcv.z, fa);
+}
+
+constexpr RgbaColor HsvColor::ToRgba() const noexcept
+{
+ float r = MathUtils::Abs(h * 6 - 3) - 1;
+ float g = 2 - MathUtils::Abs(h * 6 - 2);
+ float b = 2 - MathUtils::Abs(h * 6 - 4);
+
+ auto rgb = Vec3f{
+ std::clamp(r, 0.0f, 1.0f),
+ std::clamp(g, 0.0f, 1.0f),
+ std::clamp(b, 0.0f, 1.0f),
+ };
+ auto vc = (rgb - Vec3f{ 0, 0, 0 }) * s + Vec3f{ 1, 1, 1 } * v;
+
+ return RgbaColor(vc.x, vc.y, vc.z, a);
+}
diff --git a/app/source/Cplt/Utils/Hash.hpp b/app/source/Cplt/Utils/Hash.hpp
new file mode 100644
index 0000000..cf7713a
--- /dev/null
+++ b/app/source/Cplt/Utils/Hash.hpp
@@ -0,0 +1,15 @@
+#pragma once
+
+#include <cstddef>
+#include <functional>
+
+namespace HashUtils {
+
+template <class T>
+void Combine(size_t& seed, const T& v)
+{
+ std::hash<T> hasher;
+ seed ^= hasher(v) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
+}
+
+} // namespace HashUtils
diff --git a/app/source/Cplt/Utils/I18n.hpp b/app/source/Cplt/Utils/I18n.hpp
new file mode 100644
index 0000000..895856a
--- /dev/null
+++ b/app/source/Cplt/Utils/I18n.hpp
@@ -0,0 +1,10 @@
+#pragma once
+
+#include <Cplt/Utils/Macros.hpp>
+
+#if !defined(TARGET_LOCALE)
+# define I18N_TEXT(defaultText, name) defaultText
+#else
+# include TARGET_LOCALE_FILE
+# define I18N_TEXT(defaultText, name) name
+#endif
diff --git a/app/source/Cplt/Utils/IO/Archive.cpp b/app/source/Cplt/Utils/IO/Archive.cpp
new file mode 100644
index 0000000..f6e7b27
--- /dev/null
+++ b/app/source/Cplt/Utils/IO/Archive.cpp
@@ -0,0 +1,57 @@
+#include "Archive.hpp"
+
+constexpr uint8_t kMagicNumbers[] = { 0x98, 0xd8, 0xa4, 0x65, 0x18, 0xa2, 0xd6, 0xa0 };
+constexpr size_t kMagicNumberCount = std::size(kMagicNumbers);
+
+constexpr uint8_t kByteOrderMark = []() {
+ switch (std::endian::native) {
+ case std::endian::little: return 0;
+ case std::endian::big: return 1;
+ }
+}();
+
+std::span<const uint8_t, 8> DataArchive::GetMagicNumbers()
+{
+ return std::span<const uint8_t, 8>{ kMagicNumbers };
+}
+
+std::optional<InputDataStream> DataArchive::LoadFile(const std::filesystem::path& path)
+{
+ InputFileStream fileStream(path);
+ fileStream.SetReadInSize(1024);
+ InputDataStream stream(std::move(fileStream));
+
+ uint8_t magicNumbers[kMagicNumberCount];
+ stream.ReadBytes(kMagicNumberCount, magicNumbers);
+
+ for (size_t i = 0; i < kMagicNumberCount; ++i) {
+ if (magicNumbers[i] != kMagicNumbers[i]) {
+ return {};
+ }
+ }
+
+ uint8_t byteOrderMark;
+ stream.Read(byteOrderMark);
+
+ switch (byteOrderMark) {
+ case 0: stream.SetEndianness(std::endian::little); break;
+ case 1: stream.SetEndianness(std::endian::big); break;
+ default: std::abort();
+ }
+
+ return stream;
+}
+
+std::optional<OutputDataStream> DataArchive::SaveFile(const std::filesystem::path& path)
+{
+ OutputFileStream fileStream(path, OutputFileStream::TruncateFile);
+ fileStream.SetMaxBufferSize(1024);
+ OutputDataStream stream(std::move(fileStream));
+
+ stream.WriteBytes(kMagicNumberCount, kMagicNumbers);
+ stream.Write(kByteOrderMark);
+
+ stream.SetEndianness(std::endian::native);
+
+ return stream;
+}
diff --git a/app/source/Cplt/Utils/IO/Archive.hpp b/app/source/Cplt/Utils/IO/Archive.hpp
new file mode 100644
index 0000000..7632f3b
--- /dev/null
+++ b/app/source/Cplt/Utils/IO/Archive.hpp
@@ -0,0 +1,18 @@
+#pragma once
+
+#include <Cplt/Utils/IO/DataStream.hpp>
+
+#include <cstdint>
+#include <filesystem>
+#include <optional>
+#include <span>
+
+class DataArchive
+{
+public:
+ static std::span<const uint8_t, 8> GetMagicNumbers();
+
+ // TODO more complete impl
+ static std::optional<InputDataStream> LoadFile(const std::filesystem::path& path);
+ static std::optional<OutputDataStream> SaveFile(const std::filesystem::path& path);
+};
diff --git a/app/source/Cplt/Utils/IO/CstdioFile.cpp b/app/source/Cplt/Utils/IO/CstdioFile.cpp
new file mode 100644
index 0000000..c414dfb
--- /dev/null
+++ b/app/source/Cplt/Utils/IO/CstdioFile.cpp
@@ -0,0 +1,36 @@
+#include "CstdioFile.hpp"
+
+#include <Cplt/Utils/Macros.hpp>
+
+#pragma push_macro("MODE_STRING")
+#undef MODE_STRING
+
+#if defined(_WIN32)
+# define MODE_STRING(x) L##x
+#else
+# define MODE_STRING(x) x
+#endif
+
+namespace CPLT_UNITY_ID {
+auto GetModeString(FileUtils::IoMode mode)
+{
+ switch (mode) {
+ case FileUtils::IM_Read: return MODE_STRING("rb");
+ case FileUtils::IM_WriteAppend: return MODE_STRING("ab");
+ case FileUtils::IM_WriteTruncate: return MODE_STRING("wb");
+ }
+ return MODE_STRING("");
+}
+} // namespace CPLT_UNITY_ID
+
+#pragma pop_macro("MODE_STRING")
+
+FILE* FileUtils::OpenCstdioFile(const std::filesystem::path& path, IoMode mode)
+{
+#ifdef _WIN32
+ // std::filesystem::path::c_str() returns `const wchar_t*` under Windows, because NT uses UTF-16 natively
+ return _wfopen(path.c_str(), ::GetModeString(mode));
+#else
+ return fopen(path.c_str(), CPLT_UNITY_ID::GetModeString(mode));
+#endif
+}
diff --git a/app/source/Cplt/Utils/IO/CstdioFile.hpp b/app/source/Cplt/Utils/IO/CstdioFile.hpp
new file mode 100644
index 0000000..e863dd5
--- /dev/null
+++ b/app/source/Cplt/Utils/IO/CstdioFile.hpp
@@ -0,0 +1,17 @@
+#pragma once
+
+#include <cstdio>
+#include <filesystem>
+
+namespace FileUtils {
+
+enum IoMode
+{
+ IM_Read,
+ IM_WriteAppend,
+ IM_WriteTruncate,
+};
+
+FILE* OpenCstdioFile(const std::filesystem::path& path, IoMode mode);
+
+} // namespace FileUtils
diff --git a/app/source/Cplt/Utils/IO/DataStream.cpp b/app/source/Cplt/Utils/IO/DataStream.cpp
new file mode 100644
index 0000000..c0797e3
--- /dev/null
+++ b/app/source/Cplt/Utils/IO/DataStream.cpp
@@ -0,0 +1,283 @@
+#include "DataStream.hpp"
+
+#include <bit>
+#include <limits>
+#include <utility>
+
+static_assert(std::numeric_limits<float>::is_iec559, "Non IEE754/IEC559 'float' is not supported.");
+static_assert(std::numeric_limits<double>::is_iec559, "Non IEE754/IEC559 'double' is not supported.");
+
+static_assert(std::endian::native == std::endian::little || std::endian::native == std::endian::big, "Mixed endian is not supported.");
+
+// Reading/writing signed integer byte-by-byte is fine, since the representation got fixed to 2's complements in C++20
+
+static uint16_t ByteSwap(uint16_t n)
+{
+ auto bytes = reinterpret_cast<std::byte*>(n);
+ std::swap(bytes[0], bytes[1]);
+ return n;
+}
+
+static uint32_t ByteSwap(uint32_t n)
+{
+#ifdef _MSC_VER
+ return _byteswap_ulong(n);
+#else
+ return __builtin_bswap32(n);
+#endif
+}
+
+static uint64_t ByteSwap(uint64_t n)
+{
+#ifdef _MSC_VER
+ return _byteswap_uint64(n);
+#else
+ return __builtin_bswap64(n);
+#endif
+}
+
+template <class TSigned>
+static TSigned ByteSwap(TSigned n)
+{
+ using Unsigned = std::make_unsigned_t<TSigned>;
+
+ auto swapped = ::ByteSwap(std::bit_cast<Unsigned>(n));
+ return std::bit_cast<TSigned>(swapped);
+}
+
+std::endian BaseDataStream::GetEndianness() const
+{
+ return mEndian;
+}
+
+void BaseDataStream::SetEndianness(std::endian endianness)
+{
+ mEndian = endianness;
+}
+
+InputDataStream::InputDataStream(InputFileStream stream)
+ : mBackend{ std::move(stream) }
+{
+}
+
+void InputDataStream::ReadBytes(size_t byteCount, std::byte* buffer)
+{
+ mBackend.ReadBytes(static_cast<std::streamsize>(byteCount), reinterpret_cast<std::byte*>(buffer));
+}
+
+void InputDataStream::ReadBytes(size_t byteCount, char* buffer)
+{
+ mBackend.ReadBytes(static_cast<std::streamsize>(byteCount), reinterpret_cast<std::byte*>(buffer));
+}
+
+void InputDataStream::ReadBytes(size_t byteCount, signed char* buffer)
+{
+ mBackend.ReadBytes(static_cast<std::streamsize>(byteCount), reinterpret_cast<std::byte*>(buffer));
+}
+
+void InputDataStream::ReadBytes(size_t byteCount, unsigned char* buffer)
+{
+ mBackend.ReadBytes(static_cast<std::streamsize>(byteCount), reinterpret_cast<std::byte*>(buffer));
+}
+
+void InputDataStream::Read(int8_t& n)
+{
+ // sizeof() of a reference type yields the size of the reference
+ mBackend.ReadBytes(sizeof(n), reinterpret_cast<std::byte*>(&n));
+}
+
+void InputDataStream::Read(int16_t& n)
+{
+ int16_t tmp;
+ mBackend.ReadBytes(sizeof(tmp), reinterpret_cast<std::byte*>(&tmp));
+ if (GetEndianness() != std::endian::native) {
+ n = ::ByteSwap(tmp);
+ } else {
+ n = tmp;
+ }
+}
+
+void InputDataStream::Read(int32_t& n)
+{
+ int32_t tmp;
+ mBackend.ReadBytes(sizeof(tmp), reinterpret_cast<std::byte*>(&tmp));
+ if (GetEndianness() != std::endian::native) {
+ n = ::ByteSwap(tmp);
+ } else {
+ n = tmp;
+ }
+}
+
+void InputDataStream::Read(int64_t& n)
+{
+ int64_t tmp;
+ mBackend.ReadBytes(sizeof(tmp), reinterpret_cast<std::byte*>(&tmp));
+ if (GetEndianness() != std::endian::native) {
+ n = ::ByteSwap(tmp);
+ } else {
+ n = tmp;
+ }
+}
+
+void InputDataStream::Read(uint8_t& n)
+{
+ mBackend.ReadBytes(sizeof(n), reinterpret_cast<std::byte*>(&n));
+}
+
+void InputDataStream::Read(uint16_t& n)
+{
+ uint16_t tmp;
+ mBackend.ReadBytes(sizeof(tmp), reinterpret_cast<std::byte*>(&tmp));
+ if (GetEndianness() != std::endian::native) {
+ n = ::ByteSwap(tmp);
+ } else {
+ n = tmp;
+ }
+}
+
+void InputDataStream::Read(uint32_t& n)
+{
+ uint32_t tmp;
+ mBackend.ReadBytes(sizeof(tmp), reinterpret_cast<std::byte*>(&tmp));
+ if (GetEndianness() != std::endian::native) {
+ n = ::ByteSwap(tmp);
+ } else {
+ n = tmp;
+ }
+}
+
+void InputDataStream::Read(uint64_t& n)
+{
+ uint64_t tmp;
+ mBackend.ReadBytes(sizeof(tmp), reinterpret_cast<std::byte*>(&tmp));
+ if (GetEndianness() != std::endian::native) {
+ n = ::ByteSwap(tmp);
+ } else {
+ n = tmp;
+ }
+}
+
+void InputDataStream::Read(float& n)
+{
+ uint32_t buffer;
+ Read(buffer);
+
+ if (GetEndianness() != std::endian::native) {
+ buffer = ::ByteSwap(buffer);
+ }
+
+ n = std::bit_cast<float>(buffer);
+}
+
+void InputDataStream::Read(double& n)
+{
+ uint64_t buffer;
+ Read(buffer);
+
+ if (GetEndianness() != std::endian::native) {
+ buffer = ::ByteSwap(buffer);
+ }
+
+ n = std::bit_cast<double>(buffer);
+}
+
+OutputDataStream::OutputDataStream(OutputFileStream stream)
+ : mBackend{ std::move(stream) }
+{
+}
+
+void OutputDataStream::WriteBytes(size_t byteCount, const std::byte* buffer)
+{
+ mBackend.WriteBytes(static_cast<std::streamsize>(byteCount), reinterpret_cast<const std::byte*>(buffer));
+}
+
+void OutputDataStream::WriteBytes(size_t byteCount, const char* buffer)
+{
+ mBackend.WriteBytes(static_cast<std::streamsize>(byteCount), reinterpret_cast<const std::byte*>(buffer));
+}
+
+void OutputDataStream::WriteBytes(size_t byteCount, const signed char* buffer)
+{
+ mBackend.WriteBytes(static_cast<std::streamsize>(byteCount), reinterpret_cast<const std::byte*>(buffer));
+}
+
+void OutputDataStream::WriteBytes(size_t byteCount, const unsigned char* buffer)
+{
+ mBackend.WriteBytes(static_cast<std::streamsize>(byteCount), reinterpret_cast<const std::byte*>(buffer));
+}
+
+void OutputDataStream::Write(int8_t n)
+{
+ mBackend.WriteBytes(sizeof(n), reinterpret_cast<const std::byte*>(&n));
+}
+
+void OutputDataStream::Write(int16_t n)
+{
+ if (GetEndianness() != std::endian::native) {
+ n = ::ByteSwap(n);
+ }
+ mBackend.WriteBytes(sizeof(n), reinterpret_cast<const std::byte*>(&n));
+}
+
+void OutputDataStream::Write(int32_t n)
+{
+ if (GetEndianness() != std::endian::native) {
+ n = ::ByteSwap(n);
+ }
+ mBackend.WriteBytes(sizeof(n), reinterpret_cast<const std::byte*>(&n));
+}
+
+void OutputDataStream::Write(int64_t n)
+{
+ if (GetEndianness() != std::endian::native) {
+ n = ::ByteSwap(n);
+ }
+ mBackend.WriteBytes(sizeof(n), reinterpret_cast<const std::byte*>(&n));
+}
+
+void OutputDataStream::Write(uint8_t n)
+{
+ mBackend.WriteBytes(sizeof(n), reinterpret_cast<const std::byte*>(&n));
+}
+
+void OutputDataStream::Write(uint16_t n)
+{
+ if (GetEndianness() != std::endian::native) {
+ n = ::ByteSwap(n);
+ }
+ mBackend.WriteBytes(sizeof(n), reinterpret_cast<const std::byte*>(&n));
+}
+
+void OutputDataStream::Write(uint32_t n)
+{
+ if (GetEndianness() != std::endian::native) {
+ n = ::ByteSwap(n);
+ }
+ mBackend.WriteBytes(sizeof(n), reinterpret_cast<const std::byte*>(&n));
+}
+
+void OutputDataStream::Write(uint64_t n)
+{
+ if (GetEndianness() != std::endian::native) {
+ n = ::ByteSwap(n);
+ }
+ mBackend.WriteBytes(sizeof(n), reinterpret_cast<const std::byte*>(&n));
+}
+
+void OutputDataStream::Write(float n)
+{
+ auto buffer = std::bit_cast<uint32_t>(n);
+ if (GetEndianness() != std::endian::native) {
+ buffer = ::ByteSwap(buffer);
+ }
+ mBackend.WriteBytes(sizeof(buffer), reinterpret_cast<const std::byte*>(&buffer));
+}
+
+void OutputDataStream::Write(double n)
+{
+ auto buffer = std::bit_cast<uint64_t>(n);
+ if (GetEndianness() != std::endian::native) {
+ buffer = ::ByteSwap(buffer);
+ }
+ mBackend.WriteBytes(sizeof(buffer), reinterpret_cast<const std::byte*>(&buffer));
+}
diff --git a/app/source/Cplt/Utils/IO/DataStream.hpp b/app/source/Cplt/Utils/IO/DataStream.hpp
new file mode 100644
index 0000000..133adc2
--- /dev/null
+++ b/app/source/Cplt/Utils/IO/DataStream.hpp
@@ -0,0 +1,210 @@
+#pragma once
+
+#include "FileStream.hpp"
+#include <Cplt/fwd.hpp>
+
+#include <bit>
+#include <cstddef>
+#include <cstdint>
+#include <span>
+
+class BaseDataStream
+{
+private:
+ std::endian mEndian = std::endian::big;
+
+public:
+ std::endian GetEndianness() const;
+ void SetEndianness(std::endian endianness);
+};
+
+class InputDataStream : public BaseDataStream
+{
+private:
+ InputFileStream mBackend;
+
+public:
+ static constexpr bool IsSerializer()
+ {
+ return false;
+ }
+
+ InputDataStream(InputFileStream stream);
+
+ void ReadBytes(size_t byteCount, std::byte* buffer);
+ void ReadBytes(size_t byteCount, char* buffer);
+ void ReadBytes(size_t byteCount, signed char* buffer);
+ void ReadBytes(size_t byteCount, unsigned char* buffer);
+
+ template <class TInserter>
+ void ReadBytes(size_t byteCount, TInserter&& inserter)
+ {
+ for (size_t i = 0; i < byteCount; ++i) {
+ uint8_t byte;
+ Read(byte);
+
+ inserter = byte;
+ }
+ }
+
+ void Read(int8_t& n);
+ void Read(int16_t& n);
+ void Read(int32_t& n);
+ void Read(int64_t& n);
+
+ void Read(uint8_t& n);
+ void Read(uint16_t& n);
+ void Read(uint32_t& n);
+ void Read(uint64_t& n);
+
+ void Read(float& n);
+ void Read(double& n);
+
+ template <class TEnum>
+ requires std::is_enum_v<TEnum>
+ void ReadEnum(TEnum& e)
+ {
+ std::underlying_type_t<TEnum> n;
+ Read(n);
+ e = static_cast<TEnum>(e);
+ }
+
+ template <class TObject>
+ void ReadObject(TObject& obj)
+ {
+ obj.ReadFromDataStream(*this);
+ }
+
+ template <class TAdapter, class TObject>
+ void ReadObjectAdapted(TObject& obj)
+ {
+ TAdapter::ReadFromDataStream(*this, obj);
+ }
+
+public:
+ // Proxy functions for writing templated IO functions
+
+ template <class T>
+ void Bytes(size_t byteCount, T* buffer)
+ {
+ ReadBytes(byteCount, buffer);
+ }
+
+ template <class T>
+ void Value(T& t)
+ {
+ Read(t);
+ }
+
+ template <class T>
+ void Enum(T& t)
+ {
+ ReadEnum(t);
+ }
+
+ template <class T>
+ void Object(T& obj)
+ {
+ ReadObject(obj);
+ }
+
+ template <class TAdapter, class TObject>
+ void ObjectAdapted(TObject& obj)
+ {
+ ReadObjectAdapted<TAdapter>(obj);
+ }
+};
+
+class OutputDataStream : public BaseDataStream
+{
+private:
+ OutputFileStream mBackend;
+
+public:
+ static constexpr bool IsSerializer()
+ {
+ return true;
+ }
+
+ OutputDataStream(OutputFileStream stream);
+
+ void WriteBytes(size_t byteCount, const std::byte* buffer);
+ void WriteBytes(size_t byteCount, const char* buffer);
+ void WriteBytes(size_t byteCount, const signed char* buffer);
+ void WriteBytes(size_t byteCount, const unsigned char* buffer);
+
+ template <class TIterator>
+ void WriteBytes(TIterator&& begin, TIterator&& end)
+ {
+ for (; begin != end; ++begin) {
+ uint8_t byte = *begin;
+ Write(byte);
+ }
+ }
+
+ void Write(int8_t n);
+ void Write(int16_t n);
+ void Write(int32_t n);
+ void Write(int64_t n);
+
+ void Write(uint8_t n);
+ void Write(uint16_t n);
+ void Write(uint32_t n);
+ void Write(uint64_t n);
+
+ void Write(float n);
+ void Write(double n);
+
+ template <class TEnum>
+ requires std::is_enum_v<TEnum>
+ void WriteEnum(TEnum e)
+ {
+ auto n = static_cast<std::underlying_type_t<TEnum>>(e);
+ Write(n);
+ }
+
+ template <class TObject>
+ void WriteObject(const TObject& obj)
+ {
+ obj.WriteToDataStream(*this);
+ }
+
+ template <class TAdapter, class TObject>
+ void WriteObjectAdapted(const TObject& obj)
+ {
+ TAdapter::WriteToDataStream(*this, obj);
+ }
+
+public:
+ // Proxy functions for writing templated IO functions
+
+ template <class T>
+ void Bytes(size_t byteCount, T* buffer)
+ {
+ WriteBytes(byteCount, buffer);
+ }
+
+ template <class T>
+ void Value(T t)
+ {
+ Write(t);
+ }
+
+ template <class T>
+ void Enum(T t)
+ {
+ WriteEnum(t);
+ }
+
+ template <class T>
+ void Object(T& obj)
+ {
+ WriteObject(obj);
+ }
+
+ template <class TAdapter, class TObject>
+ void ObjectAdapted(TObject& obj)
+ {
+ WriteObjectAdapted<TAdapter>(obj);
+ }
+};
diff --git a/app/source/Cplt/Utils/IO/FileStream.cpp b/app/source/Cplt/Utils/IO/FileStream.cpp
new file mode 100644
index 0000000..8b83712
--- /dev/null
+++ b/app/source/Cplt/Utils/IO/FileStream.cpp
@@ -0,0 +1,7 @@
+#include "FileStream.hpp"
+
+#if defined(CPLT_FILESTREAM_USE_CSTDIO)
+# include "FileStream_Cstdio.inl"
+#else
+# include "FileStream_Custom.inl"
+#endif
diff --git a/app/source/Cplt/Utils/IO/FileStream.hpp b/app/source/Cplt/Utils/IO/FileStream.hpp
new file mode 100644
index 0000000..453ddbe
--- /dev/null
+++ b/app/source/Cplt/Utils/IO/FileStream.hpp
@@ -0,0 +1,97 @@
+#pragma once
+
+#include <cstddef>
+#include <cstdint>
+#include <filesystem>
+#include <memory>
+
+// TODO switch to custom when unit tests are ready and bugs are fixed
+#define CPLT_FILESTREAM_USE_CSTDIO
+
+struct IoResult
+{
+ enum ErrorKind
+ {
+ ERR_None,
+ ERR_PermissionDenied,
+ ERR_UnexpectedEof,
+ ERR_Unsupported,
+ ERR_OutOfSpace,
+ ERR_Other,
+ };
+
+ ErrorKind Error;
+ uint32_t SystemError;
+ size_t BytesMoved;
+};
+
+class InputFileStream
+{
+private:
+#if defined(CPLT_FILESTREAM_USE_CSTDIO)
+ FILE* mFile;
+#else
+ alignas(void*) char mOsFileHandle[sizeof(void*)];
+
+ // mBuffer is always mReadInSize size
+ std::unique_ptr<std::byte[]> mBuffer;
+ int mReadInSize = 1024;
+
+ int mFirstByteIdx = 0;
+ int mAvailableBytes = 0;
+
+ bool mEof = false;
+#endif
+
+public:
+ InputFileStream(const std::filesystem::path& path);
+ ~InputFileStream();
+
+ InputFileStream(const InputFileStream&) = delete;
+ InputFileStream& operator=(const InputFileStream&) = delete;
+ InputFileStream(InputFileStream&&);
+ InputFileStream& operator=(InputFileStream&&);
+
+ int GetReadInSize() const;
+ void SetReadInSize(int size);
+
+ bool IsEof() const;
+
+ IoResult ReadBytes(size_t bufferLength, std::byte* buffer);
+};
+
+class OutputFileStream
+{
+public:
+ enum WriteMode
+ {
+ AppendFile,
+ TruncateFile,
+ };
+
+private:
+#if defined(CPLT_FILESTREAM_USE_CSTDIO)
+ FILE* mFile;
+#else
+ alignas(void*) char mOsFileHandle[sizeof(void*)];
+ std::unique_ptr<std::byte[]> mBuffer;
+ int mMaxBufferSize = 1024;
+ int mCurrentBufferSize = 0;
+#endif
+
+public:
+ OutputFileStream(const std::filesystem::path& path, WriteMode mode);
+ ~OutputFileStream();
+
+ OutputFileStream(const OutputFileStream&) = delete;
+ OutputFileStream& operator=(const OutputFileStream&) = delete;
+ OutputFileStream(OutputFileStream&&);
+ OutputFileStream& operator=(OutputFileStream&&);
+
+ int GetMaxBufferSize() const;
+ void SetMaxBufferSize(int maxSize);
+
+ IoResult WriteBytes(size_t bufferLength, const std::byte* buffer);
+
+ void FlushBuffer();
+};
diff --git a/app/source/Cplt/Utils/IO/FileStream_Cstdio.inl b/app/source/Cplt/Utils/IO/FileStream_Cstdio.inl
new file mode 100644
index 0000000..ff2ca01
--- /dev/null
+++ b/app/source/Cplt/Utils/IO/FileStream_Cstdio.inl
@@ -0,0 +1,126 @@
+// Note: included by FileStream.cpp conditionally, not compiled separately
+#include "FileStream.hpp"
+
+#include <Cplt/Utils/IO/CstdioFile.hpp>
+
+#include <cstdio>
+#include <filesystem>
+
+namespace fs = std::filesystem;
+
+InputFileStream::InputFileStream(const fs::path& path)
+ : mFile{ FileUtils::OpenCstdioFile(path, FileUtils::IM_Read) }
+{
+}
+
+InputFileStream::~InputFileStream()
+{
+ if (mFile) {
+ std::fclose(mFile);
+ }
+}
+
+InputFileStream::InputFileStream(InputFileStream&& that)
+ : mFile{ that.mFile }
+{
+ that.mFile = nullptr;
+}
+
+InputFileStream& InputFileStream::operator=(InputFileStream&& that)
+{
+ if (this == &that) return *this;
+
+ if (mFile) {
+ std::fclose(mFile);
+ }
+ mFile = that.mFile;
+ that.mFile = nullptr;
+
+ return *this;
+}
+
+OutputFileStream::OutputFileStream(const fs::path& path, WriteMode mode)
+{
+ switch (mode) {
+ case AppendFile: mFile = FileUtils::OpenCstdioFile(path, FileUtils::IM_WriteAppend); break;
+ case TruncateFile: mFile = FileUtils::OpenCstdioFile(path, FileUtils::IM_WriteTruncate); break;
+ }
+}
+
+OutputFileStream::~OutputFileStream()
+{
+ if (mFile) {
+ std::fclose(mFile);
+ }
+}
+
+OutputFileStream::OutputFileStream(OutputFileStream&& that)
+ : mFile{ that.mFile }
+{
+ that.mFile = nullptr;
+}
+
+OutputFileStream& OutputFileStream::operator=(OutputFileStream&& that)
+{
+ if (this == &that) return *this;
+
+ if (mFile) {
+ std::fclose(mFile);
+ }
+ mFile = that.mFile;
+ that.mFile = nullptr;
+
+ return *this;
+}
+
+int InputFileStream::GetReadInSize() const
+{
+ return 0;
+}
+
+void InputFileStream::SetReadInSize(int size)
+{
+ // No-op
+}
+
+bool InputFileStream::IsEof() const
+{
+ return std::feof(mFile);
+}
+
+IoResult InputFileStream::ReadBytes(size_t bufferLength, std::byte* buffer)
+{
+ auto bytesRead = std::fread(buffer, 1, bufferLength, mFile);
+
+ return {
+ .Error = IoResult::ERR_None,
+ .SystemError = 0,
+ .BytesMoved = bytesRead,
+ };
+}
+
+int OutputFileStream::GetMaxBufferSize() const
+{
+ return 0;
+}
+
+void OutputFileStream::SetMaxBufferSize(int maxSize)
+{
+ // No-op
+}
+
+IoResult OutputFileStream::WriteBytes(size_t bufferLength, const std::byte* buffer)
+{
+ auto bytesWritten = std::fwrite(buffer, 1, bufferLength, mFile);
+
+ return IoResult{
+ .Error = IoResult::ERR_None,
+ .SystemError = 0,
+ .BytesMoved = bytesWritten,
+ };
+}
+
+void OutputFileStream::FlushBuffer()
+{
+ // No-op
+}
diff --git a/app/source/Cplt/Utils/IO/FileStream_Custom.inl b/app/source/Cplt/Utils/IO/FileStream_Custom.inl
new file mode 100644
index 0000000..004dd01
--- /dev/null
+++ b/app/source/Cplt/Utils/IO/FileStream_Custom.inl
@@ -0,0 +1,358 @@
+// Note: included by FileStream.cpp conditionally, not compiled separately
+#include "FileStream.hpp"
+
+#include <cstring>
+#include <filesystem>
+#include <iostream>
+
+namespace fs = std::filesystem;
+
+#if defined(_WIN32)
+# define WIN32_LEAN_AND_MEAN
+# define NOMINMAX
+# include <Windows.h>
+
+InputFileStream::InputFileStream(const fs::path& path)
+ : mOsFileHandle{ 0 }
+{
+ auto handle = reinterpret_cast<HANDLE*>(mOsFileHandle);
+
+ *handle = CreateFileW(
+ path.c_str(), // fs::path::c_str() returns a wide string on Windows
+ GENERIC_READ,
+ /* No sharing */ 0,
+ /* Use default security*/ nullptr,
+ OPEN_EXISTING,
+ FILE_ATTRIBUTE_NORMAL,
+ /* No attribute template */ nullptr);
+
+ // TODO handle error
+}
+
+InputFileStream::~InputFileStream()
+{
+ auto handle = reinterpret_cast<HANDLE*>(mOsFileHandle);
+ CloseHandle(*handle);
+}
+
+OutputFileStream::OutputFileStream(const fs::path& path, WriteMode mode)
+ : mOsFileHandle{ 0 }
+{
+ auto handle = reinterpret_cast<HANDLE*>(mOsFileHandle);
+
+ DWORD creationDisposition;
+ switch (mode) {
+ case AppendFile: creationDisposition = OPEN_ALWAYS; break;
+ case TruncateFile: creationDisposition = CREATE_ALWAYS; break;
+ }
+
+ *handle = CreateFileW(
+ path.c_str(),
+ GENERIC_WRITE,
+ /* No sharing */ 0,
+ /* Use default security*/ nullptr,
+ creationDisposition,
+ FILE_ATTRIBUTE_NORMAL,
+ /* No attribute template */ nullptr);
+
+ // TODO handle error
+}
+
+OutputFileStream::~OutputFileStream()
+{
+ auto handle = reinterpret_cast<HANDLE*>(mOsFileHandle);
+ CloseHandle(*handle);
+}
+
+static IoResult::ErrorKind MapErrorCodeToIoResult(DWORD error)
+{
+ switch (error) {
+ // TODO
+
+ default:
+ std::cerr << "Unimplemented win32 error code " << error << ", report bug immediately.\n";
+ std::abort();
+ }
+}
+
+static IoResult ReadBytesDirect(HANDLE hFile, size_t byteCount, std::byte* bytes)
+{
+ DWORD bytesRead;
+ BOOL result = ReadFile(hFile, bytes, byteCount, &bytesRead, nullptr);
+
+ if (result) {
+ return IoResult{
+ .Error = IoResult::ERR_None,
+ .SystemError = 0,
+ .BytesMoved = bytesRead,
+ };
+ } else {
+ DWORD errorCode = GetLastError();
+ return IoResult{
+ .Error = ::MapErrorCodeToIoResult(errorCode),
+ .SystemError = errorCode,
+ .BytesMoved = bytesRead,
+ };
+ }
+}
+
+static IoResult WriteBytesDirect(HANDLE hFile, size_t byteCount, const std::byte* bytes)
+{
+ DWORD bytesWritten;
+ BOOL result = WriteFile(hFile, bytes, byteCount, &bytesWritten, nullptr);
+
+ if (result) {
+ return IoResult{
+ .Error = IoResult::ERR_None,
+ .SystemError = 0,
+ .BytesMoved = bytesWritten,
+ };
+ } else {
+ DWORD errorCode = GetLastError();
+ return IoResult{
+ .Error = ::MapErrorCodeToIoResult(errorCode),
+ .SystemError = errorCode,
+ .BytesMoved = bytesWritten,
+ };
+ }
+}
+
+#elif defined(__APPLE__) || defined(__linux__)
+# include <fcntl.h>
+# include <sys/stat.h>
+# include <sys/types.h>
+# include <unistd.h>
+
+InputFileStream::InputFileStream(const fs::path& path)
+ : mOsFileHandle{ 0 }
+{
+ auto fd = reinterpret_cast<int*>(mOsFileHandle);
+ *fd = open(path.c_str(), O_RDONLY);
+}
+
+InputFileStream::~InputFileStream()
+{
+ auto fd = reinterpret_cast<int*>(mOsFileHandle);
+ close(*fd);
+}
+
+OutputFileStream::OutputFileStream(const fs::path& path, WriteMode mode)
+ : mOsFileHandle{ 0 }
+{
+ auto fd = reinterpret_cast<int*>(mOsFileHandle);
+
+ int flags = O_WRONLY | O_CREAT;
+ switch (mode) {
+ case AppendFile: flags |= O_APPEND; break;
+ case TruncateFile: flags |= O_TRUNC; break;
+ }
+
+ *fd = open(path.c_str(), flags, 0644);
+}
+
+OutputFileStream::~OutputFileStream()
+{
+ auto fd = reinterpret_cast<int*>(mOsFileHandle);
+ close(*fd);
+}
+
+static IoResult::ErrorKind MapErrnoToIoResult(int err)
+{
+ switch (err) {
+ // TODO
+ case EFAULT: return IoResult::ERR_UnexpectedEof;
+ case EPERM: return IoResult::ERR_PermissionDenied;
+ case ENOSPC: return IoResult::ERR_OutOfSpace;
+ case EIO: return IoResult::ERR_Other;
+
+ default:
+ std::cerr << "Unimplemented POSIX errno " << err << ", report bug immediately.\n";
+ std::abort();
+ }
+}
+
+static IoResult ReadBytesDirect(const char* osFileHandle, size_t byteCount, std::byte* bytes)
+{
+ int fd = *reinterpret_cast<const int*>(osFileHandle);
+ int status = read(fd, bytes, byteCount);
+
+ if (status == -1) {
+ int err = errno;
+ return IoResult{
+ .Error = ::MapErrnoToIoResult(err),
+ .SystemError = (uint32_t)err,
+ .BytesMoved = 0,
+ };
+ } else {
+ return IoResult{
+ .Error = IoResult::ERR_None,
+ .SystemError = 0,
+ .BytesMoved = (size_t)status, // Equal to number of bytes read
+ };
+ }
+}
+
+static IoResult WriteBytesDirect(const char* osFileHandle, size_t byteCount, const std::byte* bytes)
+{
+ int fd = *reinterpret_cast<const int*>(osFileHandle);
+ int status = write(fd, bytes, byteCount);
+
+ if (status == -1) {
+ int err = errno;
+ return IoResult{
+ .Error = ::MapErrnoToIoResult(err),
+ .SystemError = (uint32_t)err,
+ .BytesMoved = 0,
+ };
+ } else {
+ return IoResult{
+ .Error = IoResult::ERR_None,
+ .SystemError = 0,
+ .BytesMoved = (size_t)status, // Equal to number of bytes read
+ };
+ }
+}
+
+#else
+# error "Unsupported target platform."
+#endif
+
+int InputFileStream::GetReadInSize() const
+{
+ return mReadInSize;
+}
+
+void InputFileStream::SetReadInSize(int size)
+{
+ if (size > mReadInSize) {
+ mReadInSize = size;
+ mBuffer = std::make_unique<std::byte[]>(size);
+ }
+}
+
+bool InputFileStream::IsEof() const
+{
+ return mEof;
+}
+
+IoResult InputFileStream::ReadBytes(size_t bufferLength, std::byte* buffer)
+{
+ // TODO reduce duplicated code
+
+ auto bytesMoved = std::min<size_t>(mAvailableBytes, bufferLength);
+
+ // On first call after construction, mFirstByteIdx will equal to mReadInSize, i.e. bytesAvailable == 0
+ // and this call to std::memcpy will be no-op
+ std::memcpy(buffer, &mBuffer[mFirstByteIdx], bytesMoved);
+ mFirstByteIdx += (int)bytesMoved;
+ mAvailableBytes -= (int)bytesMoved;
+ buffer += bytesMoved;
+
+ size_t bytesLeft = bufferLength - bytesMoved;
+ if (bytesLeft > mReadInSize) {
+ // Our buffer can't handle rest of the request, just skip the buffering step
+
+ // Read rest of the data into buffer
+ {
+ auto result = ::ReadBytesDirect(mOsFileHandle, bytesLeft, buffer);
+ bytesMoved += result.BytesMoved;
+
+ if (result.Error == IoResult::ERR_None) {
+ if (result.BytesMoved < mReadInSize) {
+ mEof = true;
+ }
+ } else {
+ goto end;
+ }
+ }
+
+ // Refill our buffer
+ {
+ auto result = ::ReadBytesDirect(mOsFileHandle, mReadInSize, mBuffer.get());
+ mFirstByteIdx = 0;
+ mAvailableBytes = (int)result.BytesMoved;
+
+ if (result.Error == IoResult::ERR_None) {
+ if (result.BytesMoved < mReadInSize) {
+ mEof = true;
+ }
+ } else {
+ goto end;
+ }
+ }
+ } else if (bytesLeft > 0) {
+ // Our buffer can handle rest of the request, first buffer than supply the requested data
+
+ // Refill our buffer
+ {
+ auto result = ::ReadBytesDirect(mOsFileHandle, mReadInSize, mBuffer.get());
+ mFirstByteIdx = 0;
+ mAvailableBytes = (int)result.BytesMoved;
+
+ if (result.Error == IoResult::ERR_None) {
+ if (result.BytesMoved < mReadInSize) {
+ mEof = true;
+ }
+ } else {
+ goto end;
+ }
+ }
+
+ // Copy data into buffer
+ {
+ std::memcpy(buffer, &mBuffer[mFirstByteIdx], bytesLeft);
+ mFirstByteIdx += (int)bytesLeft;
+ bytesMoved += bytesLeft;
+ buffer += bytesLeft;
+ }
+ } else {
+ // Request completed already
+ }
+
+end:
+ return IoResult{
+ .Error = IoResult::ERR_None,
+ .SystemError = 0,
+ .BytesMoved = bytesMoved,
+ };
+}
+
+int OutputFileStream::GetMaxBufferSize() const
+{
+ return mMaxBufferSize;
+}
+
+void OutputFileStream::SetMaxBufferSize(int maxSize)
+{
+ FlushBuffer();
+ if (maxSize > mMaxBufferSize) {
+ mMaxBufferSize = maxSize;
+ mBuffer = std::make_unique<std::byte[]>(maxSize);
+ }
+}
+
+IoResult OutputFileStream::WriteBytes(size_t bufferLength, const std::byte* buffer)
+{
+ if (bufferLength + mCurrentBufferSize > mMaxBufferSize) {
+ FlushBuffer();
+
+ if (bufferLength > mMaxBufferSize) {
+ return ::WriteBytesDirect(mOsFileHandle, bufferLength, buffer);
+ }
+ }
+
+ std::memcpy(mBuffer.get() + mCurrentBufferSize, buffer, bufferLength);
+ mCurrentBufferSize += (int)bufferLength;
+
+ return IoResult{
+ .Error = IoResult::ERR_None,
+ .SystemError = 0,
+ .BytesMoved = bufferLength,
+ };
+}
+
+void OutputFileStream::FlushBuffer()
+{
+ ::WriteBytesDirect(mOsFileHandle, mCurrentBufferSize, mBuffer.get());
+ mCurrentBufferSize = 0;
+}
diff --git a/app/source/Cplt/Utils/IO/Helper.hpp b/app/source/Cplt/Utils/IO/Helper.hpp
new file mode 100644
index 0000000..7a84103
--- /dev/null
+++ b/app/source/Cplt/Utils/IO/Helper.hpp
@@ -0,0 +1,43 @@
+#pragma once
+
+#include <Cplt/Utils/IO/DataStream.hpp>
+
+namespace DataStreamAdapters {
+
+/// Helper to invoke either Read() or ReadObject().
+/// This is intended for writing IO adapters, users that's writing IO logic shouldn't using this - it increases compile time while reducing readability.
+template <class TAdapter, class T>
+void ReadHelper(InputDataStream& stream, T& t)
+{
+ if constexpr (!std::is_same_v<TAdapter, void>) {
+ stream.ReadObjectAdapted<TAdapter>(t);
+ } else if constexpr (requires(T tt, InputDataStream ss) { ss.Read(tt); }) {
+ stream.Read(t);
+ } else if constexpr (requires(T tt, InputDataStream ss) { ss.ReadEnum(tt); }) {
+ stream.ReadEnum(t);
+ } else if constexpr (requires(T tt, InputDataStream ss) { ss.ReadObject(tt); }) {
+ stream.ReadObject(t);
+ } else {
+ static_assert(false && sizeof(T), "This type is neither a 'value' nor an 'object'.");
+ }
+}
+
+/// Helper to invoke either Write() or WriteObject().
+/// This is intended for writing IO adapters, users that's writing IO logic shouldn't using this - it increases compile time while reducing readability.
+template <class TAdapter, class T>
+void WriteHelper(OutputDataStream& stream, T& t)
+{
+ if constexpr (!std::is_same_v<TAdapter, void>) {
+ stream.WriteObjectAdapted<TAdapter>(t);
+ } else if constexpr (requires(T tt, OutputDataStream ss) { ss.Write(tt); }) {
+ stream.Write(t);
+ } else if constexpr (requires(T tt, OutputDataStream ss) { ss.WriteEnum(tt); }) {
+ stream.WriteEnum(t);
+ } else if constexpr (requires(T tt, OutputDataStream ss) { ss.WriteObject(tt); }) {
+ stream.WriteObject(t);
+ } else {
+ static_assert(false && sizeof(T), "This type is neither a 'value' nor an 'object'.");
+ }
+}
+
+} // namespace DataStreamAdapters
diff --git a/app/source/Cplt/Utils/IO/StringIntegration.hpp b/app/source/Cplt/Utils/IO/StringIntegration.hpp
new file mode 100644
index 0000000..66f42b0
--- /dev/null
+++ b/app/source/Cplt/Utils/IO/StringIntegration.hpp
@@ -0,0 +1,37 @@
+#pragma once
+
+#include <Cplt/Utils/IO/DataStream.hpp>
+
+#include <iterator>
+#include <string>
+#include <string_view>
+
+namespace DataStreamAdapters {
+struct String
+{
+ static void ReadFromDataStream(InputDataStream& stream, std::string& str)
+ {
+ uint64_t size;
+ stream.Read(size);
+
+ str = {};
+ str.reserve(size);
+ stream.ReadBytes(size, std::back_inserter(str));
+ }
+
+ static void WriteToDataStream(OutputDataStream& stream, const std::string& str)
+ {
+ stream.Write((uint64_t)str.size());
+ stream.WriteBytes(str.size(), str.data());
+ }
+};
+
+struct StringView
+{
+ static void WriteToDataStream(OutputDataStream& stream, const std::string_view& str)
+ {
+ stream.Write((uint64_t)str.size());
+ stream.WriteBytes(str.size(), str.data());
+ }
+};
+} // namespace DataStreamAdapters
diff --git a/app/source/Cplt/Utils/IO/TslArrayIntegration.hpp b/app/source/Cplt/Utils/IO/TslArrayIntegration.hpp
new file mode 100644
index 0000000..b585bee
--- /dev/null
+++ b/app/source/Cplt/Utils/IO/TslArrayIntegration.hpp
@@ -0,0 +1,50 @@
+#pragma once
+
+#include <Cplt/Utils/IO/DataStream.hpp>
+#include <Cplt/Utils/IO/Helper.hpp>
+#include <Cplt/Utils/IO/StringIntegration.hpp>
+
+#include <tsl/array_map.h>
+#include <tsl/array_set.h>
+#include <string>
+#include <type_traits>
+
+// TODO support custom key types
+
+namespace DataStreamAdapters {
+template <class TAdapter = void>
+struct TslArrayMap
+{
+ template <class TValue>
+ static void ReadFromDataStream(InputDataStream& stream, tsl::array_map<char, TValue>& map)
+ {
+ static_assert(std::is_default_constructible_v<TValue>);
+ static_assert(std::is_move_constructible_v<TValue>);
+
+ uint64_t size;
+ stream.Read(size);
+ map.reserve(size);
+
+ for (uint64_t i = 0; i < size; ++i) {
+ std::string key;
+ stream.ReadObjectAdapted<DataStreamAdapters::String>(key);
+
+ TValue value;
+ ReadHelper<TAdapter>(stream, value);
+
+ map.insert(key, std::move(value));
+ }
+ }
+
+ template <class TValue>
+ static void WriteToDataStream(OutputDataStream& stream, const tsl::array_map<char, TValue>& map)
+ {
+ stream.Write((uint64_t)map.size());
+
+ for (auto it = map.begin(); it != map.end(); ++it) {
+ stream.WriteObjectAdapted<DataStreamAdapters::StringView>(it.key_sv());
+ WriteHelper<TAdapter>(stream, it.value());
+ }
+ }
+};
+} // namespace DataStreamAdapters
diff --git a/app/source/Cplt/Utils/IO/TslRobinIntegration.hpp b/app/source/Cplt/Utils/IO/TslRobinIntegration.hpp
new file mode 100644
index 0000000..bdea505
--- /dev/null
+++ b/app/source/Cplt/Utils/IO/TslRobinIntegration.hpp
@@ -0,0 +1,78 @@
+#pragma once
+
+#include <Cplt/Utils/IO/DataStream.hpp>
+#include <Cplt/Utils/IO/Helper.hpp>
+
+#include <tsl/robin_map.h>
+#include <tsl/robin_set.h>
+#include <type_traits>
+
+namespace DataStreamAdapters {
+template <class TKeyAdapter = void, class TValueAdapter = void>
+struct TslRobinMap
+{
+ template <class TKey, class TValue>
+ static void ReadFromDataStream(InputDataStream& stream, tsl::robin_map<TKey, TValue>& map)
+ {
+ static_assert(std::is_default_constructible_v<TValue>);
+ static_assert(std::is_move_constructible_v<TValue>);
+
+ uint64_t size;
+ stream.Read(size);
+ map.reserve(size);
+
+ for (uint64_t i = 0; i < size; ++i) {
+ TKey key;
+ ReadHelper<TKeyAdapter>(stream, key);
+
+ TValue value;
+ ReadHelper<TValueAdapter>(stream, value);
+
+ map.insert(std::move(key), std::move(value));
+ }
+ }
+
+ template <class TKey, class TValue>
+ static void WriteToDataStream(OutputDataStream& stream, const tsl::robin_map<TKey, TValue>& map)
+ {
+ stream.Write((uint64_t)map.size());
+
+ for (auto it = map.begin(); it != map.end(); ++it) {
+ WriteHelper<TKeyAdapter>(stream, it.key());
+ WriteHelper<TValueAdapter>(stream, it.value());
+ }
+ }
+};
+
+template <class TAdapter = void>
+struct TslRobinSet
+{
+ template <class TElement>
+ static void ReadFromDataStream(InputDataStream& stream, tsl::robin_set<TElement>& set)
+ {
+ static_assert(std::is_default_constructible_v<TElement>);
+ static_assert(std::is_move_constructible_v<TElement>);
+
+ uint64_t size;
+ stream.Read(size);
+ set.reserve(size);
+
+ for (uint64_t i = 0; i < size; ++i) {
+ TElement element;
+ ReadHelper<TAdapter>(stream, element);
+
+ set.insert(std::move(element));
+ }
+ }
+
+ template <class TElement>
+ static void WriteToDataStream(OutputDataStream& stream, const tsl::robin_set<TElement>& set)
+ {
+ stream.Write((uint64_t)set.size());
+
+ for (auto& element : set) {
+ WriteHelper<TAdapter>(stream, element);
+ }
+ }
+};
+} // namespace DataStreamAdapters
diff --git a/app/source/Cplt/Utils/IO/UuidIntegration.hpp b/app/source/Cplt/Utils/IO/UuidIntegration.hpp
new file mode 100644
index 0000000..20c1e7e
--- /dev/null
+++ b/app/source/Cplt/Utils/IO/UuidIntegration.hpp
@@ -0,0 +1,27 @@
+#pragma once
+
+#include <Cplt/Utils/IO/DataStream.hpp>
+#include <Cplt/Utils/UUID.hpp>
+
+#include <cstddef>
+#include <cstdint>
+#include <iterator>
+
+namespace DataStreamAdapters {
+struct Uuid
+{
+ static void ReadFromDataStream(InputDataStream& stream, uuids::uuid& uuid)
+ {
+ uint8_t buffer[16];
+ stream.ReadBytes(16, buffer);
+
+ uuid = uuids::uuid(gsl::span<uint8_t, 16>{ buffer });
+ }
+
+ static void WriteToDataStream(OutputDataStream& stream, const uuids::uuid& uuid)
+ {
+ auto gslSpan = uuid.as_bytes();
+ stream.WriteBytes(gslSpan.size(), gslSpan.data());
+ }
+};
+} // namespace DataStreamAdapters
diff --git a/app/source/Cplt/Utils/IO/VectorIntegration.hpp b/app/source/Cplt/Utils/IO/VectorIntegration.hpp
new file mode 100644
index 0000000..93967f6
--- /dev/null
+++ b/app/source/Cplt/Utils/IO/VectorIntegration.hpp
@@ -0,0 +1,42 @@
+#pragma once
+
+#include <Cplt/Utils/IO/DataStream.hpp>
+#include <Cplt/Utils/IO/Helper.hpp>
+
+#include <type_traits>
+#include <vector>
+
+namespace DataStreamAdapters {
+template <class TAdapter = void>
+struct Vector
+{
+ template <class TElement>
+ static void ReadFromDataStream(InputDataStream& stream, std::vector<TElement>& vec)
+ {
+ static_assert(std::is_default_constructible_v<TElement>);
+ static_assert(std::is_move_constructible_v<TElement>);
+
+ uint64_t size;
+ stream.Read(size);
+
+ vec.clear();
+ vec.reserve(size);
+
+ for (uint64_t i = 0; i < size; ++i) {
+ TElement element;
+ ReadHelper<TAdapter>(stream, element);
+
+ vec.push_back(std::move(element));
+ }
+ }
+
+ template <class TElement>
+ static void WriteToDataStream(OutputDataStream& stream, const std::vector<TElement>& vec)
+ {
+ stream.Write((uint64_t)vec.size());
+ for (auto& element : vec) {
+ WriteHelper<TAdapter>(stream, element);
+ }
+ }
+};
+} // namespace DataStreamAdapters
diff --git a/app/source/Cplt/Utils/IO/fwd.hpp b/app/source/Cplt/Utils/IO/fwd.hpp
new file mode 100644
index 0000000..9f1492b
--- /dev/null
+++ b/app/source/Cplt/Utils/IO/fwd.hpp
@@ -0,0 +1,13 @@
+#pragma once
+
+// Archive.hpp
+class DataArchive;
+
+// BaseDataStream.hpp
+class BaseDataStream;
+class InputDataStream;
+class OutputDataStream;
+
+// FileStream.hpp
+class InputFileStream;
+class OutputFileStream;
diff --git a/app/source/Cplt/Utils/Macros.hpp b/app/source/Cplt/Utils/Macros.hpp
new file mode 100644
index 0000000..6958ed1
--- /dev/null
+++ b/app/source/Cplt/Utils/Macros.hpp
@@ -0,0 +1,13 @@
+#pragma once
+
+#define STRINGIFY_IMPL(text) #text
+#define STRINGIFY(text) STRINGIFY_IMPL(text)
+
+#define CONCAT_IMPL(a, b) a##b
+#define CONCAT(a, b) CONCAT_IMPL(a, b)
+#define CONCAT_3(a, b, c) CONCAT(a, CONCAT(b, c))
+#define CONCAT_4(a, b, c, d) CONCAT(CONCAT(a, b), CONCAT(c, d))
+
+#define UNIQUE_NAME(prefix) CONCAT(prefix, __COUNTER__)
+#define UNIQUE_NAME_LINE(prefix) CONCAT(prefix, __LINE__)
+#define DISCARD UNIQUE_NAME(_discard)
diff --git a/app/source/Cplt/Utils/Math.hpp b/app/source/Cplt/Utils/Math.hpp
new file mode 100644
index 0000000..da53da2
--- /dev/null
+++ b/app/source/Cplt/Utils/Math.hpp
@@ -0,0 +1,11 @@
+#pragma once
+
+namespace MathUtils {
+
+template <class T>
+constexpr T Abs(T t)
+{
+ return t < 0 ? -t : t;
+}
+
+} // namespace MathUtils
diff --git a/app/source/Cplt/Utils/RTTI.hpp b/app/source/Cplt/Utils/RTTI.hpp
new file mode 100644
index 0000000..86b1e2c
--- /dev/null
+++ b/app/source/Cplt/Utils/RTTI.hpp
@@ -0,0 +1,49 @@
+#pragma once
+
+#include <cassert>
+
+template <class T, class TBase>
+bool is_a(TBase* t)
+{
+ assert(t != nullptr);
+ return T::IsInstance(t);
+}
+
+template <class T, class TBase>
+bool is_a_nullable(TBase* t)
+{
+ if (t) {
+ return is_a<T, TBase>(t);
+ } else {
+ return false;
+ }
+}
+
+template <class T, class TBase>
+T* dyn_cast(TBase* t)
+{
+ assert(t != nullptr);
+ if (T::IsInstance(t)) {
+ return static_cast<T*>(t);
+ } else {
+ return nullptr;
+ }
+}
+
+template <class T, class TBase>
+const T* dyn_cast(const TBase* t)
+{
+ assert(t != nullptr);
+ if (T::IsInstance(t)) {
+ return static_cast<const T*>(t);
+ } else {
+ return nullptr;
+ }
+}
+
+template <class T, class TBase>
+T* dyn_cast_nullable(TBase* t)
+{
+ if (!t) return nullptr;
+ return dyn_cast<T, TBase>(t);
+}
diff --git a/app/source/Cplt/Utils/ScopeGuard.hpp b/app/source/Cplt/Utils/ScopeGuard.hpp
new file mode 100644
index 0000000..f2b7f46
--- /dev/null
+++ b/app/source/Cplt/Utils/ScopeGuard.hpp
@@ -0,0 +1,39 @@
+#pragma once
+
+#include <Cplt/Utils/Macros.hpp>
+
+#include <utility>
+
+template <class TCleanupFunc>
+class ScopeGuard
+{
+private:
+ TCleanupFunc mFunc;
+ bool mDismissed = false;
+
+public:
+ /// Specifically left this implicit so that constructs like
+ /// \code
+ /// ScopeGuard sg = [&]() { res.Cleanup(); };
+ /// \endcode
+ /// would work. It is highly discourage and unlikely that one would want to use ScopeGuard as a function
+ /// parameter, so the normal argument that implicit conversion are harmful doesn't really apply here.
+ ScopeGuard(TCleanupFunc func)
+ : mFunc{ std::move(func) }
+ {
+ }
+
+ ~ScopeGuard()
+ {
+ if (!mDismissed) {
+ mFunc();
+ }
+ }
+
+ void Dismiss() noexcept
+ {
+ mDismissed = true;
+ }
+};
+
+#define DEFER ScopeGuard UNIQUE_NAME(scopeGuard) = [&]()
diff --git a/app/source/Cplt/Utils/Sigslot.cpp b/app/source/Cplt/Utils/Sigslot.cpp
new file mode 100644
index 0000000..1132dfb
--- /dev/null
+++ b/app/source/Cplt/Utils/Sigslot.cpp
@@ -0,0 +1,233 @@
+#include "Sigslot.hpp"
+
+#include <doctest/doctest.h>
+
+bool SignalStub::Connection::IsOccupied() const
+{
+ return id != InvalidId;
+}
+
+SignalStub::SignalStub(IWrapper& wrapper)
+ : mWrapper{ &wrapper }
+{
+}
+
+SignalStub::~SignalStub()
+{
+ RemoveAllConnections();
+}
+
+std::span<const SignalStub::Connection> SignalStub::GetConnections() const
+{
+ return mConnections;
+}
+
+SignalStub::Connection& SignalStub::InsertConnection(SlotGuard* guard)
+{
+ Connection* result;
+ int size = static_cast<int>(mConnections.size());
+ for (int i = 0; i < size; ++i) {
+ auto& conn = mConnections[i];
+ if (!conn.IsOccupied()) {
+ result = &conn;
+ result->id = i;
+ goto setup;
+ }
+ }
+
+ mConnections.push_back(Connection{});
+ result = &mConnections.back();
+ result->id = size;
+
+setup:
+ if (guard) {
+ result->guard = guard;
+ result->slotId = guard->InsertConnection(*this, result->id);
+ }
+ return *result;
+}
+
+void SignalStub::RemoveConnection(int id)
+{
+ if (id >= 0 && id < mConnections.size()) {
+ auto& conn = mConnections[id];
+ if (conn.IsOccupied()) {
+ mWrapper->RemoveFunction(conn.id);
+ if (conn.guard) {
+ conn.guard->RemoveConnection(conn.slotId);
+ }
+
+ conn.guard = nullptr;
+ conn.slotId = SignalStub::InvalidId;
+ conn.id = SignalStub::InvalidId;
+ }
+ }
+}
+
+void SignalStub::RemoveConnectionFor(SlotGuard& guard)
+{
+ guard.RemoveConnectionFor(*this);
+}
+
+void SignalStub::RemoveAllConnections()
+{
+ for (size_t i = 0; i < mConnections.size(); ++i) {
+ RemoveConnection(i);
+ }
+}
+
+SlotGuard::SlotGuard()
+{
+}
+
+SlotGuard::~SlotGuard()
+{
+ DisconnectAll();
+}
+
+void SlotGuard::DisconnectAll()
+{
+ for (auto& conn : mConnections) {
+ if (conn.stub) {
+ // Also calls SlotGuard::removeConnection, our copy of the data will be cleared in it
+ conn.stub->RemoveConnection(conn.stubId);
+ }
+ }
+}
+
+int SlotGuard::InsertConnection(SignalStub& stub, int stubId)
+{
+ int size = static_cast<int>(mConnections.size());
+ for (int i = 0; i < size; ++i) {
+ auto& conn = mConnections[i];
+ if (!conn.stub) {
+ conn.stub = &stub;
+ conn.stubId = stubId;
+ return i;
+ }
+ }
+
+ mConnections.push_back(Connection{});
+ auto& conn = mConnections.back();
+ conn.stub = &stub;
+ conn.stubId = stubId;
+ return size;
+}
+
+void SlotGuard::RemoveConnectionFor(SignalStub& stub)
+{
+ for (auto& conn : mConnections) {
+ if (conn.stub == &stub) {
+ conn.stub->RemoveConnection(conn.stubId);
+ }
+ }
+}
+
+void SlotGuard::RemoveConnection(int slotId)
+{
+ mConnections[slotId] = {};
+}
+
+TEST_CASE("Signal connect and disconnect")
+{
+ Signal<> sig;
+
+ int counter = 0;
+ int id = sig.Connect([&]() { counter++; });
+
+ sig();
+ CHECK(counter == 1);
+
+ sig();
+ CHECK(counter == 2);
+
+ sig.Disconnect(id);
+ sig();
+ CHECK(counter == 2);
+}
+
+TEST_CASE("Signal with parameters")
+{
+ Signal<int> sig;
+
+ int counter = 0;
+ int id = sig.Connect([&](int i) { counter += i; });
+
+ sig(1);
+ CHECK(counter == 1);
+
+ sig(0);
+ CHECK(counter == 1);
+
+ sig(4);
+ CHECK(counter == 5);
+
+ sig.Disconnect(id);
+ sig(1);
+ CHECK(counter == 5);
+}
+
+TEST_CASE("Signal disconnectAll()")
+{
+ Signal<> sig;
+
+ int counter1 = 0;
+ int counter2 = 0;
+ sig.Connect([&]() { counter1++; });
+ sig.Connect([&]() { counter2++; });
+
+ sig();
+ CHECK(counter1 == 1);
+ CHECK(counter2 == 1);
+
+ sig();
+ CHECK(counter1 == 2);
+ CHECK(counter2 == 2);
+
+ sig.DisconnectAll();
+ sig();
+ CHECK(counter1 == 2);
+ CHECK(counter2 == 2);
+}
+
+TEST_CASE("SlotGuard auto-disconnection")
+{
+ int counter1 = 0;
+ int counter2 = 0;
+ Signal<> sig;
+
+ {
+ SlotGuard guard;
+ sig.Connect(guard, [&]() { counter1 += 1; });
+ sig.Connect(guard, [&]() { counter2 += 1; });
+
+ sig();
+ CHECK(counter1 == 1);
+ CHECK(counter2 == 1);
+
+ sig();
+ CHECK(counter1 == 2);
+ CHECK(counter2 == 2);
+ }
+
+ sig();
+ CHECK(counter1 == 2);
+ CHECK(counter2 == 2);
+}
+
+TEST_CASE("Signal destruct before SlotGuard")
+{
+ int counter = 0;
+ SlotGuard guard;
+
+ {
+ Signal<> sig2;
+ sig2.Connect(guard, [&]() { counter++; });
+
+ sig2();
+ CHECK(counter == 1);
+ }
+
+ // Shouldn't error
+ guard.DisconnectAll();
+}
diff --git a/app/source/Cplt/Utils/Sigslot.hpp b/app/source/Cplt/Utils/Sigslot.hpp
new file mode 100644
index 0000000..a4ab94e
--- /dev/null
+++ b/app/source/Cplt/Utils/Sigslot.hpp
@@ -0,0 +1,165 @@
+#pragma once
+
+#include <Cplt/Utils/fwd.hpp>
+
+#include <cstddef>
+#include <functional>
+#include <span>
+#include <utility>
+#include <vector>
+
+class SignalStub
+{
+public:
+ /// Non-template interface for Signal<T...> to implement (a barrier to stop template
+ /// arguments propagation).
+ class IWrapper
+ {
+ public:
+ virtual ~IWrapper() = default;
+ virtual void RemoveFunction(int id) = 0;
+ };
+
+ enum
+ {
+ InvalidId = -1,
+ };
+
+ struct Connection
+ {
+ SlotGuard* guard;
+ int slotId;
+ int id = InvalidId; // If `InvalidId`, then this "spot" is unused
+
+ bool IsOccupied() const;
+ };
+
+private:
+ std::vector<Connection> mConnections;
+ IWrapper* mWrapper;
+
+private:
+ template <class...>
+ friend class Signal;
+ friend class SlotGuard;
+
+ SignalStub(IWrapper& wrapper);
+ ~SignalStub();
+
+ SignalStub(const SignalStub&) = delete;
+ SignalStub& operator=(const SignalStub&) = delete;
+ SignalStub(SignalStub&&) = default;
+ SignalStub& operator=(SignalStub&&) = default;
+
+ std::span<const Connection> GetConnections() const;
+ Connection& InsertConnection(SlotGuard* guard = nullptr);
+ void RemoveConnection(int id);
+ void RemoveConnectionFor(SlotGuard& guard);
+ void RemoveAllConnections();
+};
+
+template <class... TArgs>
+class Signal : public SignalStub::IWrapper
+{
+private:
+ // Must be in this order so that mFunctions is still intact when mStub's destructor runs
+ std::vector<std::function<void(TArgs...)>> mFunctions;
+ SignalStub mStub;
+
+public:
+ Signal()
+ : mStub(*this)
+ {
+ }
+
+ virtual ~Signal() = default;
+
+ Signal(const Signal&) = delete;
+ Signal& operator=(const Signal&) = delete;
+ Signal(Signal&&) = default;
+ Signal& operator=(Signal&&) = default;
+
+ void operator()(TArgs... args)
+ {
+ for (auto& conn : mStub.GetConnections()) {
+ if (conn.IsOccupied()) {
+ mFunctions[conn.id](std::forward<TArgs>(args)...);
+ }
+ }
+ }
+
+ template <class TFunction>
+ int Connect(TFunction slot)
+ {
+ auto& conn = mStub.InsertConnection();
+ mFunctions.resize(std::max(mFunctions.size(), (size_t)conn.id + 1));
+ mFunctions[conn.id] = std::move(slot);
+ return conn.id;
+ }
+
+ template <class TFunction>
+ int Connect(SlotGuard& guard, TFunction slot)
+ {
+ auto& conn = mStub.InsertConnection(&guard);
+ mFunctions.resize(std::max(mFunctions.size(), (size_t)conn.id + 1));
+ mFunctions[conn.id] = std::move(slot);
+ return conn.id;
+ }
+
+ void Disconnect(int id)
+ {
+ mStub.RemoveConnection(id);
+ }
+
+ void DisconnectFor(SlotGuard& guard)
+ {
+ mStub.RemoveConnectionFor(guard);
+ }
+
+ void DisconnectAll()
+ {
+ mStub.RemoveAllConnections();
+ }
+
+ virtual void RemoveFunction(int id)
+ {
+ mFunctions[id] = {};
+ }
+};
+
+/// Automatic disconnection mechanism for Signal<>.
+/// Bind connection to this guard by using the Connect(SlotGuard&, TFunction) overload.
+/// Either DisconnectAll() or the destructor disconnects all connections bound to this guard.
+class SlotGuard
+{
+private:
+ struct Connection
+ {
+ SignalStub* stub = nullptr;
+ int stubId = SignalStub::InvalidId;
+ };
+ std::vector<Connection> mConnections;
+
+public:
+ friend class SignalStub;
+ SlotGuard();
+ ~SlotGuard();
+
+ SlotGuard(const SlotGuard&) = delete;
+ SlotGuard& operator=(const SlotGuard&) = delete;
+ SlotGuard(SlotGuard&&) = default;
+ SlotGuard& operator=(SlotGuard&&) = default;
+
+ /// DisconnectBySource all connection associated with this SlotGuard.
+ void DisconnectAll();
+
+private:
+ /// \return Slot id.
+ int InsertConnection(SignalStub& stub, int stubId);
+ /// Remove the connection data in this associated with slotId. This does not invoke
+ /// the connections' stub's RemoveConnection function.
+ void RemoveConnection(int slotId);
+ /// DisconnectBySource all connections from the given stub associated with this SlotGuard.
+ /// Implementation for SignalStub::RemoveConnectionsFor(SlotGuard&)
+ void RemoveConnectionFor(SignalStub& stub);
+};
diff --git a/app/source/Cplt/Utils/Size.hpp b/app/source/Cplt/Utils/Size.hpp
new file mode 100644
index 0000000..ae38e8a
--- /dev/null
+++ b/app/source/Cplt/Utils/Size.hpp
@@ -0,0 +1,65 @@
+#pragma once
+
+#include <Cplt/Utils/Vector.hpp>
+
+template <class T>
+class Size2 {
+public:
+ T width;
+ T height;
+
+public:
+ Size2()
+ : width{ 0 }, height{ 0 } {
+ }
+
+ Size2(T width, T height)
+ : width{ width }, height{ height } {
+ }
+
+ Size2(Vec2<T> vec)
+ : width{ vec.x }, height{ vec.y }
+ {
+ }
+
+ operator Vec2<T>() const
+ {
+ return { width, height };
+ }
+
+ Vec2<T> AsVec() const
+ {
+ return { width, height };
+ }
+
+ friend bool operator==(const Size2<T>&, const Size2<T>&) = default;
+
+ template <class TTarget>
+ Size2<TTarget> Cast() const
+ {
+ return {
+ static_cast<TTarget>(width),
+ static_cast<TTarget>(height),
+ };
+ }
+};
+
+template <class T>
+Size2<T> operator+(Size2<T> a, Size2<T> b) {
+ return { a.width + b.width, a.height + b.height };
+}
+
+template <class T>
+Size2<T> operator-(Size2<T> a, Size2<T> b) {
+ return { a.width - b.width, a.height - b.height };
+}
+
+template <class T, class N>
+auto operator*(Size2<T> a, N mult) -> Size2<decltype(a.width * mult)> {
+ return { a.width * mult, a.height * mult };
+}
+
+template <class T, class N>
+auto operator/(Size2<T> a, N mult) -> Size2<decltype(a.width / mult)> {
+ return { a.width / mult, a.height / mult };
+}
diff --git a/app/source/Cplt/Utils/StandardDirectories.cpp b/app/source/Cplt/Utils/StandardDirectories.cpp
new file mode 100644
index 0000000..2202f51
--- /dev/null
+++ b/app/source/Cplt/Utils/StandardDirectories.cpp
@@ -0,0 +1,78 @@
+#include "StandardDirectories.hpp"
+
+#include <filesystem>
+#include <stdexcept>
+
+namespace fs = std::filesystem;
+
+#if defined(_WIN32)
+// https://stackoverflow.com/questions/54499256/how-to-find-the-saved-games-folder-programmatically-in-c-c
+# include <ShlObj_core.h>
+# include <objbase.h>
+# pragma comment(lib, "shell32.lib")
+# pragma comment(lib, "ole32.lib")
+
+static fs::path GetAppDataRoaming()
+{
+ PWSTR path = nullptr;
+ HRESULT hr = SHGetKnownFolderPath(FOLDERID_RoamingAppData, KF_FLAG_CREATE, nullptr, &path);
+ if (SUCCEEDED(hr)) {
+ auto dataDir = fs::path(path);
+ CoTaskMemFree(path);
+
+ fs::create_directories(dataDir);
+ return dataDir;
+ } else {
+ fs::path dataDir("~/AppData/Roaming");
+ fs::create_directories(dataDir);
+ return dataDir;
+ }
+}
+
+#elif defined(__APPLE__)
+// TODO
+#elif defined(__linux__)
+# include <cstdlib>
+
+static fs::path GetEnvVar(const char* name, const char* backup)
+{
+ if (const char* path = std::getenv(name)) {
+ fs::path dataDir(path);
+ fs::create_directories(dataDir);
+ return dataDir;
+ } else {
+ fs::path dataDir(backup);
+ fs::create_directories(dataDir);
+ return dataDir;
+ }
+}
+
+#endif
+
+const std::filesystem::path& StandardDirectories::UserData()
+{
+ static auto userDataDir = []() -> fs::path {
+#if defined(_WIN32)
+ return GetAppDataRoaming();
+#elif defined(__APPLE__)
+ // TODO where?
+#elif defined(__linux__)
+ return GetEnvVar("XDG_DATA_HOME", "~/.local/share");
+#endif
+ }();
+ return userDataDir;
+}
+
+const std::filesystem::path& StandardDirectories::UserConfig()
+{
+ static auto userConfigDir = []() -> fs::path {
+#if defined(_WIN32)
+ return GetAppDataRoaming();
+#elif defined(__APPLE__)
+ // TODO where?
+#elif defined(__linux__)
+ return GetEnvVar("XDG_CONFIG_HOME", "~/.config");
+#endif
+ }();
+ return userConfigDir;
+}
diff --git a/app/source/Cplt/Utils/StandardDirectories.hpp b/app/source/Cplt/Utils/StandardDirectories.hpp
new file mode 100644
index 0000000..4f7e5e2
--- /dev/null
+++ b/app/source/Cplt/Utils/StandardDirectories.hpp
@@ -0,0 +1,10 @@
+#pragma once
+
+#include <filesystem>
+
+namespace StandardDirectories {
+
+const std::filesystem::path& UserData();
+const std::filesystem::path& UserConfig();
+
+} // namespace StandardDirectories
diff --git a/app/source/Cplt/Utils/Time.cpp b/app/source/Cplt/Utils/Time.cpp
new file mode 100644
index 0000000..4e79ffa
--- /dev/null
+++ b/app/source/Cplt/Utils/Time.cpp
@@ -0,0 +1,29 @@
+#include "Time.hpp"
+
+#include <ctime>
+
+std::string TimeUtils::StringifyTimePoint(std::chrono::time_point<std::chrono::system_clock> tp)
+{
+ auto t = std::chrono::system_clock::to_time_t(tp);
+
+ char data[32];
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations" // C++ doesn't have std::localtime_s
+ std::strftime(data, sizeof(data), "%Y-%m-%d %H:%M:%S", std::localtime(&t));
+#pragma clang diagnostic pop
+
+ return std::string(data);
+}
+
+std::string TimeUtils::StringifyTimeStamp(int64_t timeStamp)
+{
+ if (timeStamp == 0) {
+ return "";
+ }
+
+ namespace chrono = std::chrono;
+ chrono::milliseconds d{ timeStamp };
+ chrono::time_point<chrono::system_clock> tp{ d };
+
+ return StringifyTimePoint(tp);
+}
diff --git a/app/source/Cplt/Utils/Time.hpp b/app/source/Cplt/Utils/Time.hpp
new file mode 100644
index 0000000..fbbd3b2
--- /dev/null
+++ b/app/source/Cplt/Utils/Time.hpp
@@ -0,0 +1,11 @@
+#pragma once
+
+#include <chrono>
+#include <string>
+
+namespace TimeUtils {
+
+std::string StringifyTimePoint(std::chrono::time_point<std::chrono::system_clock> tp);
+std::string StringifyTimeStamp(int64_t timeStamp);
+
+} // namespace TimeUtils
diff --git a/app/source/Cplt/Utils/UUID.hpp b/app/source/Cplt/Utils/UUID.hpp
new file mode 100644
index 0000000..9044aa6
--- /dev/null
+++ b/app/source/Cplt/Utils/UUID.hpp
@@ -0,0 +1,5 @@
+#pragma once
+
+#define WIN32_LEAN_AND_MEAN
+#define NOMINMAX
+#include <uuid.h>
diff --git a/app/source/Cplt/Utils/Variant.hpp b/app/source/Cplt/Utils/Variant.hpp
new file mode 100644
index 0000000..df2f882
--- /dev/null
+++ b/app/source/Cplt/Utils/Variant.hpp
@@ -0,0 +1,33 @@
+#pragma once
+
+#include <utility>
+#include <variant>
+
+template <class... Ts>
+struct Overloaded : Ts...
+{
+ using Ts::operator()...;
+};
+template <class... Ts>
+Overloaded(Ts...) -> Overloaded<Ts...>;
+
+template <class... Args>
+struct VariantCastProxy
+{
+ std::variant<Args...> v;
+
+ template <class... ToArgs>
+ operator std::variant<ToArgs...>() const
+ {
+ return std::visit(
+ [](auto&& arg) -> std::variant<ToArgs...> { return arg; },
+ v);
+ }
+};
+
+/// Use snake_case naming to align with `static_cast`, `dynamic_cast`, etc..
+template <class... Args>
+auto variant_cast(std::variant<Args...> v) -> VariantCastProxy<Args...>
+{
+ return { std::move(v) };
+}
diff --git a/app/source/Cplt/Utils/Vector.hpp b/app/source/Cplt/Utils/Vector.hpp
new file mode 100644
index 0000000..79f4ea2
--- /dev/null
+++ b/app/source/Cplt/Utils/Vector.hpp
@@ -0,0 +1,144 @@
+#pragma once
+
+#include <Cplt/Utils/IO/DataStream.hpp>
+
+template <class T>
+struct Vec2
+{
+ T x = 0;
+ T y = 0;
+
+ template <class TTarget>
+ Vec2<TTarget> Cast() const
+ {
+ return {
+ static_cast<TTarget>(x),
+ static_cast<TTarget>(y),
+ };
+ }
+
+ void ReadFromDataStream(InputDataStream& stream)
+ {
+ stream.Value(x);
+ stream.Value(y);
+ }
+
+ void WriteToDataStream(OutputDataStream& stream) const
+ {
+ stream.Value(x);
+ stream.Value(y);
+ }
+
+ friend constexpr bool operator==(const Vec2& a, const Vec2& b) = default;
+
+ friend constexpr Vec2 operator+(const Vec2& a, const Vec2& b) { return { a.x + b.x, a.y + b.y }; }
+ friend constexpr Vec2 operator-(const Vec2& a, const Vec2& b) { return { a.x - b.x, a.y - b.y }; }
+ friend constexpr Vec2 operator*(const Vec2& a, const Vec2& b) { return { a.x * b.x, a.y * b.y }; }
+ friend constexpr Vec2 operator/(const Vec2& a, const Vec2& b) { return { a.x / b.x, a.y / b.y }; }
+
+ friend constexpr Vec2 operator+(const Vec2& a, T n) { return { a.x + n, a.y + n }; }
+ friend constexpr Vec2 operator-(const Vec2& a, T n) { return { a.x - n, a.y - n }; }
+ friend constexpr Vec2 operator*(const Vec2& a, T n) { return { a.x * n, a.y * n }; }
+ friend constexpr Vec2 operator/(const Vec2& a, T n) { return { a.x / n, a.y / n }; }
+};
+
+using Vec2i = Vec2<int>;
+using Vec2f = Vec2<float>;
+
+template <class T>
+struct Vec3
+{
+ T x = 0;
+ T y = 0;
+ T z = 0;
+
+ template <class TTarget>
+ Vec3<TTarget> Cast() const
+ {
+ return {
+ static_cast<TTarget>(x),
+ static_cast<TTarget>(y),
+ static_cast<TTarget>(z),
+ };
+ }
+
+ void ReadFromDataStream(InputDataStream& stream)
+ {
+ stream.Value(x);
+ stream.Value(y);
+ stream.Value(z);
+ }
+
+ void WriteToDataStream(OutputDataStream& stream) const
+ {
+ stream.Value(x);
+ stream.Value(y);
+ stream.Value(z);
+ }
+
+ friend constexpr bool operator==(const Vec3& a, const Vec3& b) = default;
+
+ friend constexpr Vec3 operator+(const Vec3& a, const Vec3& b) { return { a.x + b.x, a.y + b.y, a.z + b.z }; }
+ friend constexpr Vec3 operator-(const Vec3& a, const Vec3& b) { return { a.x - b.x, a.y - b.y, a.z - b.z }; }
+ friend constexpr Vec3 operator*(const Vec3& a, const Vec3& b) { return { a.x * b.x, a.y * b.y, a.z * b.z }; }
+ friend constexpr Vec3 operator/(const Vec3& a, const Vec3& b) { return { a.x / b.x, a.y / b.y, a.z / b.z }; }
+
+ friend constexpr Vec3 operator+(const Vec3& a, T n) { return { a.x + n, a.y + n, a.z + n }; }
+ friend constexpr Vec3 operator-(const Vec3& a, T n) { return { a.x - n, a.y - n, a.z - n }; }
+ friend constexpr Vec3 operator*(const Vec3& a, T n) { return { a.x * n, a.y * n, a.z * n }; }
+ friend constexpr Vec3 operator/(const Vec3& a, T n) { return { a.x / n, a.y / n, a.z / n }; }
+};
+
+using Vec3i = Vec3<int>;
+using Vec3f = Vec3<float>;
+
+template <class T>
+struct Vec4
+{
+ T x = 0;
+ T y = 0;
+ T z = 0;
+ T w = 0;
+
+ template <class TTarget>
+ Vec4<TTarget> Cast() const
+ {
+ return {
+ static_cast<TTarget>(x),
+ static_cast<TTarget>(y),
+ static_cast<TTarget>(z),
+ static_cast<TTarget>(w),
+ };
+ }
+
+ void ReadFromDataStream(InputDataStream& stream)
+ {
+ stream.Value(x);
+ stream.Value(y);
+ stream.Value(z);
+ stream.Value(w);
+ }
+
+ void WriteToDataStream(OutputDataStream& stream) const
+ {
+ stream.Value(x);
+ stream.Value(y);
+ stream.Value(z);
+ stream.Value(w);
+ }
+
+ friend constexpr bool operator==(const Vec4& a, const Vec4& b) = default;
+
+ friend constexpr Vec4 operator+(const Vec4& a, const Vec4& b) { return { a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w }; }
+ friend constexpr Vec4 operator-(const Vec4& a, const Vec4& b) { return { a.x - b.x, a.y - b.y, a.z - b.z, a.w - b.w }; }
+ friend constexpr Vec4 operator*(const Vec4& a, const Vec4& b) { return { a.x * b.x, a.y * b.y, a.z * b.z, a.w * b.w }; }
+ friend constexpr Vec4 operator/(const Vec4& a, const Vec4& b) { return { a.x / b.x, a.y / b.y, a.z / b.z, a.w / b.w }; }
+
+ friend constexpr Vec4 operator+(const Vec4& a, T n) { return { a.x + n, a.y + n, a.z + n, a.w + n }; }
+ friend constexpr Vec4 operator-(const Vec4& a, T n) { return { a.x - n, a.y - n, a.z - n, a.w - n }; }
+ friend constexpr Vec4 operator*(const Vec4& a, T n) { return { a.x * n, a.y * n, a.z * n, a.w * n }; }
+ friend constexpr Vec4 operator/(const Vec4& a, T n) { return { a.x / n, a.y / n, a.z / n, a.w / n }; }
+};
+
+using Vec4i = Vec4<int>;
+using Vec4f = Vec4<float>;
diff --git a/app/source/Cplt/Utils/VectorHash.hpp b/app/source/Cplt/Utils/VectorHash.hpp
new file mode 100644
index 0000000..f649367
--- /dev/null
+++ b/app/source/Cplt/Utils/VectorHash.hpp
@@ -0,0 +1,46 @@
+#pragma once
+
+#include <Cplt/Utils/Hash.hpp>
+#include <Cplt/Utils/Vector.hpp>
+
+#include <cstddef>
+#include <functional>
+
+template <class T>
+struct std::hash<Vec2<T>>
+{
+ size_t operator()(const Vec2<T>& vec) const
+ {
+ size_t result;
+ HashUtils::Combine(result, vec.x);
+ HashUtils::Combine(result, vec.y);
+ return result;
+ }
+};
+
+template <class T>
+struct std::hash<Vec3<T>>
+{
+ size_t operator()(const Vec3<T>& vec) const
+ {
+ size_t result;
+ HashUtils::Combine(result, vec.x);
+ HashUtils::Combine(result, vec.y);
+ HashUtils::Combine(result, vec.z);
+ return result;
+ }
+};
+
+template <class T>
+struct std::hash<Vec4<T>>
+{
+ size_t operator()(const Vec4<T>& vec) const
+ {
+ size_t result;
+ HashUtils::Combine(result, vec.x);
+ HashUtils::Combine(result, vec.y);
+ HashUtils::Combine(result, vec.z);
+ HashUtils::Combine(result, vec.w);
+ return result;
+ }
+};
diff --git a/app/source/Cplt/Utils/fwd.hpp b/app/source/Cplt/Utils/fwd.hpp
new file mode 100644
index 0000000..366cacc
--- /dev/null
+++ b/app/source/Cplt/Utils/fwd.hpp
@@ -0,0 +1,17 @@
+#pragma once
+
+#include <Cplt/Utils/IO/fwd.hpp>
+
+// Color.hpp
+class RgbaColor;
+class HsvColor;
+
+// Sigslot.hpp
+class SignalStub;
+template <class... TArgs>
+class Signal;
+class SlotGuard;
+
+// String.hpp
+class Utf8Iterator;
+class Utf8IterableString;