diff --git a/source/ECS.hpp b/source/ECS.hpp new file mode 100644 index 0000000..b639d7e --- /dev/null +++ b/source/ECS.hpp @@ -0,0 +1,147 @@ +/* + * SPDX-FileCopyrightText: Dora "cat" + * SPDX-License-Identifier: MPL-2.0 + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at http://mozilla.org/MPL/2.0/. + */ +#ifndef GUARD_TOURMALINE_ECS_H +#define GUARD_TOURMALINE_ECS_H +#include +#include +#include +#include +#include +#include + +#include "Systems/Logging.hpp" +#include "Types.hpp" + +namespace Tourmaline::ECS { +using Entity = Tourmaline::Type::UUID; +class World; + +struct BaseComponent { +public: + virtual ~BaseComponent() = default; + const Entity &GetOwner(); + +private: + const Entity *owner; + friend World; +}; + +// Concepts +template +concept Component = std::derived_from; + +class World { +public: + // Entity + Entity CreateEntity(); + bool EntityExists(const Entity &entity) noexcept; + [[nodiscard("It is not guaranteed that an entity can always be destroyed, " + "please make " + "sure by checking the returned bool")]] + bool DestroyEntity(Entity entity); + + // Components + template + T &AddComponent(const Entity &entity, Args &&...constructionArguments) { + // Insert to entity list + auto entityIter = GetEntityIterator( + entity, + std::format( + "Cannot add component \"{}\"! Entity \"{}\" does not exist!", + typeid(T).name(), entity.asString()), + "AddComponent", Systems::Logging::LogLevel::Error); + + auto [componentIter, success] = entityIter->second.try_emplace( + typeid(T), T(std::forward(constructionArguments)...)); + Systems::Logging::Log( + std::format("Cannot add component! Component \"{}\" already exists " + "in entity \"{}\" ", + typeid(T).name(), entity.asString()), + "AddComponent", Systems::Logging::LogLevel::Error, !success); + + T &component = std::any_cast(componentIter->second); + component.owner = &entity; + return component; + } + + template + [[nodiscard("Discarding an expensive operation's result!")]] + T &GetComponent(const Entity &entity) { + auto iter = GetEntityIterator( + entity, + std::format("Can't get entity \"{}\"'s component \"{}\", since " + "entity does not exist!", + entity.asString(), typeid(T).name()), + "GetComponent", Systems::Logging::LogLevel::Error); + + auto component = iter->second.find(typeid(T)); + Systems::Logging::Log( + std::format( + "Entity \"{}\" does not have component \"{}\", cannot get it!", + entity.asString(), typeid(T).name()), + "GetComponent", Systems::Logging::LogLevel::Error, + component == iter->second.end()); + + return std::any_cast(component->second); + } + + template + [[nodiscard("Discarding an expensive operation's result!")]] + bool HasComponent(const Entity &entity) { + auto iter = GetEntityIterator( + entity, + std::format("Can't find if entity \"{}\" has component \"{}\", since " + "entity does not exist!", + entity.asString(), typeid(T).name())); + + return iter != entityComponentList.end() && + (iter->second.find(typeid(T)) != iter->second.end()); + } + + template + [[nodiscard("It is not guaranteed that a component can always be removed, " + "please make " + "sure by checking the returned bool")]] + bool RemoveComponent(const Entity &entity) { + auto entityIter = GetEntityIterator( + entity, + std::format("Cannot remove component {} from entity {}, since entity " + "does not exist!", + typeid(T).name(), entity.asString()), + "RemoveComponent", Systems::Logging::LogLevel::Warning); + if (entityIter == entityComponentList.end()) { + return false; + } + + auto componentIter = entityIter->second.find(typeid(T)); + if (componentIter == entityIter->second.end()) { + Systems::Logging::Log( + std::format("Cannot remove component {} from entity {}, since entity " + "does not have that component", + typeid(T).name(), entity.asString()), + "RemoveComponent", Systems::Logging::LogLevel::Warning); + return false; + } + + entityIter->second.erase(componentIter); + return true; + } + +private: + std::unordered_map> + entityComponentList{}; + + decltype(entityComponentList)::iterator + GetEntityIterator(const Entity &entity, const std::string &errorMessage = "", + const std::string &position = "", + Tourmaline::Systems::Logging::LogLevel severity = + Systems::Logging::LogLevel::Warning); +}; +} // namespace Tourmaline::ECS +#endif diff --git a/source/ECS/Component.cpp b/source/ECS/Component.cpp new file mode 100644 index 0000000..6c8ca52 --- /dev/null +++ b/source/ECS/Component.cpp @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: Dora "cat" + * SPDX-License-Identifier: MPL-2.0 + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "../ECS.hpp" + +using namespace Tourmaline::ECS; + +const Entity &BaseComponent::GetOwner() { return *this->owner; } diff --git a/source/ECS/World.cpp b/source/ECS/World.cpp new file mode 100644 index 0000000..02a6e21 --- /dev/null +++ b/source/ECS/World.cpp @@ -0,0 +1,55 @@ +/* + * SPDX-FileCopyrightText: Dora "cat" + * SPDX-License-Identifier: MPL-2.0 + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "../ECS.hpp" +#include "../Systems/Random.hpp" + +using namespace Tourmaline::ECS; + +Entity World::CreateEntity() { + auto [iterator, success] = + entityComponentList.try_emplace(Systems::Random::GenerateUUID()); + + Systems::Logging::Log("Failed to create an entity! Possibly by incredible " + "luck generated already existing UUID?", + "CreateEntity", Systems::Logging::LogLevel::Critical, + !success); + return iterator->first; +} + +bool World::EntityExists(const Entity &entity) noexcept { + return entityComponentList.find(entity) != entityComponentList.end(); +} + +bool World::DestroyEntity(Entity entity) { + auto entityIter = GetEntityIterator( + entity, + std::format("Cannot delete entity \"{}\", it does not exist!", + entity.asString()), + "DestroyEntity"); + + if (entityIter == entityComponentList.end()) { + return false; + } + + entityComponentList.erase(entityIter); + return true; +} + +// Very repetitive code +decltype(World::entityComponentList)::iterator +World::GetEntityIterator(const Entity &entity, const std::string &errorMessage, + const std::string &position, + Tourmaline::Systems::Logging::LogLevel severity) { + auto iter = entityComponentList.find(entity); + + Systems::Logging::Log(errorMessage, "GetEntityIterator/" + position, severity, + iter == entityComponentList.end()); + return iter; +} diff --git a/source/Systems/Implementation/Logging.cpp b/source/Systems/Implementation/Logging.cpp new file mode 100644 index 0000000..37f0e30 --- /dev/null +++ b/source/Systems/Implementation/Logging.cpp @@ -0,0 +1,61 @@ +/* + * SPDX-FileCopyrightText: Dora "cat" + * SPDX-License-Identifier: MPL-2.0 + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "../Logging.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Tourmaline::Systems; + +// This is what happens when it takes you 50 years to implement +// reflections to a language +std::array Logging::LogLevelToString{ + "Critical", "Error", "Warning", "Info", "Debug", "Trace"}; +std::fstream Logging::File; + +void Logging::LogToFile(std::string File) { + if (File == "") { + const auto now = std::chrono::system_clock::now(); + File = std::format("Tourmaline-{:%Y-%j}.txt", now); + } + Logging::File.open(File, std::fstream::out); + + if (Logging::File.fail()) { + throw std::runtime_error("FAILED! Could not open or create the file: " + + File + "!\n" + strerror(errno)); + } +} + +void Logging::Log(const std::string &message, const std::string &position, + Logging::LogLevel severity, bool assertion) { + if (assertion) [[likely]] { + std::string output = + std::format("[{}@{}] {}\n", + Logging::LogLevelToString[static_cast(severity)], + position, message); + + std::print("{}", output); + if (Logging::File.is_open()) { + Logging::File.write(output.c_str(), output.size()); + Logging::File.flush(); // Terrible but necessary sadly + } + + // Error and Critical + if (severity < Logging::LogLevel::Warning) { + throw std::runtime_error(output); + } + } +} diff --git a/source/Systems/Implementation/Random.cpp b/source/Systems/Implementation/Random.cpp new file mode 100644 index 0000000..1d36cd3 --- /dev/null +++ b/source/Systems/Implementation/Random.cpp @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: Dora "cat" + * SPDX-License-Identifier: MPL-2.0 + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at http://mozilla.org/MPL/2.0/. + */ +#include "../Random.hpp" +#include +#include +#include + +using namespace Tourmaline::Systems; + +Xoshiro::Xoshiro256PP Random::generator(static_cast(time(NULL))); +Tourmaline::Type::UUID Random::GenerateUUID() { + uint64_t random_ab = generator(), random_c = generator(), hold = 0; + hold = std::rotr(random_ab, 12); + hold = hold >> 4; + hold = (hold << 4) + 0b0100; + random_ab = std::rotl(hold, 12); + random_c = ((random_c >> 2) << 2) + 2; + + return Tourmaline::Type::UUID(random_ab, random_c); +} diff --git a/source/Systems/Logging.hpp b/source/Systems/Logging.hpp new file mode 100644 index 0000000..60794d1 --- /dev/null +++ b/source/Systems/Logging.hpp @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: Dora "cat" + * SPDX-License-Identifier: MPL-2.0 + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include +#include +#include + +namespace Tourmaline::Systems { +class Logging { +public: + enum class LogLevel { + Critical = 0, + Error = 1, + Warning = 2, + Info = 3, + Debug = 4, + Trace = 5 + }; + + static void LogToFile(std::string File = ""); + static void Log(const std::string &message, + const std::string &position = "Unknown", + LogLevel severity = LogLevel::Info, bool assertion = true); + +private: + static std::fstream File; + static std::array LogLevelToString; +}; +} // namespace Tourmaline::Systems diff --git a/source/Systems/Random.hpp b/source/Systems/Random.hpp new file mode 100644 index 0000000..af8da7b --- /dev/null +++ b/source/Systems/Random.hpp @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: Dora "cat" + * SPDX-License-Identifier: MPL-2.0 + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at http://mozilla.org/MPL/2.0/. + */ + +#ifndef GUARD_TOURMALINE_RANDOM_H +#define GUARD_TOURMALINE_RANDOM_H +#include "../../libraries/random/xoshiro.h" +#include "../Types.hpp" +#include + +namespace Tourmaline::Systems { +class Random { +public: + template + requires std::is_integral_v + static T Generate(T max, T min = 0) { + return (generator() % (max - min + 1)) + min; + } + static Tourmaline::Type::UUID GenerateUUID(); + +private: + static Xoshiro::Xoshiro256PP generator; +}; +} // namespace Tourmaline::Systems +#endif diff --git a/source/Types.hpp b/source/Types.hpp new file mode 100644 index 0000000..134eb79 --- /dev/null +++ b/source/Types.hpp @@ -0,0 +1,54 @@ +/* + * SPDX-FileCopyrightText: Dora "cat" + * SPDX-License-Identifier: MPL-2.0 + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at http://mozilla.org/MPL/2.0/. + */ +#ifndef GUARD_TOURMALINE_TYPES_H +#define GUARD_TOURMALINE_TYPES_H +#include +#include +#include +namespace Tourmaline::Type { +class UUID { +public: + constexpr static uint8_t BitLength = 128; + constexpr static uint8_t QWORDLength = BitLength / 64; + constexpr static uint8_t ByteLength = BitLength / 8; + + [[nodiscard]] + std::string asString() const; + bool operator==(const UUID &rhs) const; + + UUID(uint64_t firstHalf, uint64_t secondHalf); + UUID(const std::string &uuid); + + UUID(const UUID &uuid); + UUID(UUID &&uuid) noexcept; + UUID &operator=(const UUID &uuid); + UUID &operator=(UUID &&uuid); + ~UUID() = default; + +private: + std::unique_ptr data = std::make_unique(QWORDLength); + friend struct std::hash; +}; +} // namespace Tourmaline::Type + +namespace std { +template <> struct hash { + size_t operator()(const Tourmaline::Type::UUID &uuid) const noexcept { + const auto data = uuid.data.get(); + size_t h1 = std::hash{}(data[0]); + size_t h2 = std::hash{}(data[1]); + + // Combine the two hashes + return h1 ^ (h2 << 1); + } +}; + +} // namespace std + +#endif diff --git a/source/Types/UUID.cpp b/source/Types/UUID.cpp new file mode 100644 index 0000000..6c41ff0 --- /dev/null +++ b/source/Types/UUID.cpp @@ -0,0 +1,68 @@ +/* + * SPDX-FileCopyrightText: Dora "cat" + * SPDX-License-Identifier: MPL-2.0 + * + * This Source Code Form is subject to the terms of the Mozilla Public License, + * v. 2.0. If a copy of the MPL was not distributed with this file, You can + * obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "../Types.hpp" +#include +#include +#include +#include +#include +#include + +using namespace Tourmaline::Type; +std::string UUID::asString() const { + return std::format("{:016X}{:016X}", data[0], data[1]); +} + +bool UUID::operator==(const UUID &rhs) const { + // Since size may be increased + for (uint8_t index = 0; index < QWORDLength; index++) { + if (this->data[index] != rhs.data[index]) { + return false; + } + } + return true; +} + +UUID::UUID(const UUID &uuid) { + std::memcpy(data.get(), uuid.data.get(), UUID::ByteLength); +} + +UUID &UUID::operator=(const UUID &uuid) { + if (this != &uuid) [[likely]] { + std::memcpy(data.get(), uuid.data.get(), UUID::ByteLength); + } + + return *this; +} + +UUID &UUID::operator=(UUID &&uuid) { + if (this != &uuid) [[likely]] { + data.swap(uuid.data); + } + + return *this; +} + +UUID::UUID(UUID &&uuid) noexcept { data.swap(uuid.data); } + +UUID::UUID(uint64_t firstHalf, uint64_t secondHalf) { + data[0] = firstHalf; + data[1] = secondHalf; +} + +UUID::UUID(const std::string &uuid) { + // We are assuming that it is a valid UUID, if not then somewhere else this + // UUID should cause an error + auto start = uuid.c_str(), half = start + ByteLength, + tail = half + ByteLength; // Each UUID element is 16 characters padded + + std::from_chars(start, half, data[0], 16); + std::from_chars(half, tail, data[1], 16); +}