diff options
author | rtk0c <[email protected]> | 2022-06-30 21:38:53 -0700 |
---|---|---|
committer | rtk0c <[email protected]> | 2022-06-30 21:38:53 -0700 |
commit | 7fe47a9d5b1727a61dc724523b530762f6d6ba19 (patch) | |
tree | e95be6e66db504ed06d00b72c579565bab873277 /app/source/Cplt/Utils | |
parent | 2cf952088d375ac8b2f45b144462af0953436cff (diff) |
Restructure project
Diffstat (limited to 'app/source/Cplt/Utils')
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; |