Compare commits

...

3 commits

Author SHA1 Message Date
1265b816d3 Replace ecs::container::describe() with describe_short() and describe_detailed(), and introduce entity inspection API
All checks were successful
Run tests / Run tests (push) Successful in 6m56s
2026-08-13 18:03:57 +03:00
0fecd6375b Add util/string.hpp 2026-08-13 18:02:55 +03:00
68d8e4b18a Use RAII wrapper for method recursion depth in ecs::container 2026-08-13 16:50:53 +03:00
14 changed files with 327 additions and 154 deletions

View file

@ -3,8 +3,6 @@
#include <psemek/ecs/exceptions.hpp> #include <psemek/ecs/exceptions.hpp>
#include <psemek/ecs/detail/table.hpp> #include <psemek/ecs/detail/table.hpp>
#include <typeindex>
namespace psemek::ecs namespace psemek::ecs
{ {
@ -55,6 +53,19 @@ namespace psemek::ecs
template <typename Component> template <typename Component>
Component const & get() const; Component const & get() const;
/** Obtain the component by uuid of the accessed entity.
* As the runtime type of the component is unknown,
* the function returns pointer-to-void.
*
* @param uuid The uuid of component to obtain
* @return A pointer to the component of the specified type, or a null
* pointer if the entity doesn't contain a component of this type
* @pre The accessor is valid
*/
void * get_if(util::uuid const & uuid);
void const * get_if(util::uuid const & uuid) const;
/** Check if the entity contains this component type. /** Check if the entity contains this component type.
* *
* @tparam Component The type of component to obtain * @tparam Component The type of component to obtain
@ -98,12 +109,7 @@ namespace psemek::ecs
Component const * accessor::get_if() const Component const * accessor::get_if() const
{ {
util::uuid const uuid = std::remove_const_t<Component>::uuid(); util::uuid const uuid = std::remove_const_t<Component>::uuid();
return reinterpret_cast<Component const *>(get_if(uuid));
auto column = table_->column(uuid);
if (!column)
return nullptr;
return reinterpret_cast<Component *>(column->data() + detail::stride<std::remove_const_t<Component>>() * row_);
} }
template <typename Component> template <typename Component>
@ -111,7 +117,7 @@ namespace psemek::ecs
{ {
if (auto ptr = get_if<Component>()) if (auto ptr = get_if<Component>())
return *ptr; return *ptr;
throw component_not_found_exception(typeid(std::remove_const_t<Component>), table_->entity_handles()[row_], table_->describe(row_)); throw component_not_found_exception(typeid(std::remove_const_t<Component>), table_->entity_handles()[row_], table_->describe_short());
} }
template <typename Component> template <typename Component>
@ -119,7 +125,21 @@ namespace psemek::ecs
{ {
if (auto ptr = get_if<Component const>()) if (auto ptr = get_if<Component const>())
return *ptr; return *ptr;
throw component_not_found_exception(typeid(std::remove_const_t<Component>), table_->entity_handles()[row_], table_->describe(row_)); throw component_not_found_exception(typeid(std::remove_const_t<Component>), table_->entity_handles()[row_], table_->describe_short());
}
inline void * accessor::get_if(util::uuid const & uuid)
{
return const_cast<void *>(static_cast<accessor const *>(this)->get_if(uuid));
}
inline void const * accessor::get_if(util::uuid const & uuid) const
{
auto column = table_->column(uuid);
if (!column)
return nullptr;
return column->data() + column->stride() * row_;
} }
template <typename Component> template <typename Component>

View file

@ -9,7 +9,9 @@
#include <psemek/ecs/detail/without.hpp> #include <psemek/ecs/detail/without.hpp>
#include <psemek/ecs/detail/component_registry.hpp> #include <psemek/ecs/detail/component_registry.hpp>
#include <psemek/ecs/detail/index_container.hpp> #include <psemek/ecs/detail/index_container.hpp>
#include <psemek/ecs/detail/recursion_depth_guard.hpp>
#include <psemek/ecs/accessor.hpp> #include <psemek/ecs/accessor.hpp>
#include <psemek/ecs/inspect.hpp>
#include <psemek/ecs/exceptions.hpp> #include <psemek/ecs/exceptions.hpp>
#include <psemek/ecs/statistics.hpp> #include <psemek/ecs/statistics.hpp>
#include <psemek/util/range.hpp> #include <psemek/util/range.hpp>
@ -94,14 +96,33 @@ namespace psemek::ecs
*/ */
accessor get(handle entity); accessor get(handle entity);
/** Compute a string representation of an entity, based on the contained components. /** Inspect the entity, i.e. enumerate the entitie's components.
* A component has to implement a to_string() method in order to provide extra info.
* *
* @param entity A handle to the entity to describe * @param entity A handle to the entity to inspect
* @return A string representation of an entity * @return A list of component uuids & metadata
* @pre The entity was previously obtained by a `create()` call and is `alive()` * @pre The entity was previously obtained by a `create()` call and is `alive()`
*/ */
std::string describe(handle entity) const; entity_inspection_info inspect(handle entity) const;
/** Compute a short string representation of an entity, based on the contained components.
* The short version just enumerates the component types, separated by ';'.
*
* @param entity A handle to the entity to describe
* @return A short string representation of an entity
* @pre The entity was previously obtained by a `create()` call and is `alive()`
*/
std::string describe_short(handle entity) const;
/** Compute a detailed string representation of an entity, based on the contained components.
* A component has to implement a to_string() method in order to provide extra info.
* The detailed version lists all components together with their data (as provided by
* component.to_string) in a large multi-line string.
*
* @param entity A handle to the entity to describe
* @return A detailed string representation of an entity
* @pre The entity was previously obtained by a `create()` call and is `alive()`
*/
std::string describe_detailed(handle entity) const;
/** Check if the entity can be cloned. /** Check if the entity can be cloned.
* *
@ -423,7 +444,7 @@ namespace psemek::ecs
{ {
static_assert(detail::all_different_types_v<std::remove_cvref_t<Components>...>, "all component types must be different"); static_assert(detail::all_different_types_v<std::remove_cvref_t<Components>...>, "all component types must be different");
++method_recursion_depth_; detail::recursion_depth_guard guard{method_recursion_depth_};
(register_component<std::remove_cvref_t<Components>>(), ...); (register_component<std::remove_cvref_t<Components>>(), ...);
@ -461,8 +482,6 @@ namespace psemek::ecs
finalize_method(); finalize_method();
--method_recursion_depth_;
return handle; return handle;
} }
@ -476,7 +495,7 @@ namespace psemek::ecs
currently_changing_archetype_.insert(entity); currently_changing_archetype_.insert(entity);
#endif #endif
++method_recursion_depth_; detail::recursion_depth_guard guard{method_recursion_depth_};
(register_component<Components>(), ...); (register_component<Components>(), ...);
@ -538,8 +557,6 @@ namespace psemek::ecs
#endif #endif
finalize_method(); finalize_method();
--method_recursion_depth_;
} }
template <typename ... Components> template <typename ... Components>
@ -552,7 +569,7 @@ namespace psemek::ecs
currently_changing_archetype_.insert(entity); currently_changing_archetype_.insert(entity);
#endif #endif
++method_recursion_depth_; detail::recursion_depth_guard guard{method_recursion_depth_};
auto detached_uuid_set = uuid_set_pool_.get(); auto detached_uuid_set = uuid_set_pool_.get();
(detached_uuid_set.insert(std::remove_const_t<Components>::uuid()), ...); (detached_uuid_set.insert(std::remove_const_t<Components>::uuid()), ...);
@ -614,8 +631,6 @@ namespace psemek::ecs
#endif #endif
finalize_method(); finalize_method();
--method_recursion_depth_;
} }
template <typename ... Components> template <typename ... Components>
@ -641,7 +656,7 @@ namespace psemek::ecs
{ {
static_assert(detail::all_different_types_v<std::remove_const_t<Components>...>, "all component types must be different"); static_assert(detail::all_different_types_v<std::remove_const_t<Components>...>, "all component types must be different");
++method_recursion_depth_; detail::recursion_depth_guard guard{method_recursion_depth_};
using invocable_type = typename detail::filter_with<detail::invocable, std::tuple<Components...>, Function>::type; using invocable_type = typename detail::filter_with<detail::invocable, std::tuple<Components...>, Function>::type;
@ -672,8 +687,6 @@ namespace psemek::ecs
finalize_method(); finalize_method();
--method_recursion_depth_;
return cache; return cache;
} }
@ -682,7 +695,7 @@ namespace psemek::ecs
{ {
static_assert(detail::all_different_types_v<std::remove_const_t<Components>...>, "all component types must be different"); static_assert(detail::all_different_types_v<std::remove_const_t<Components>...>, "all component types must be different");
++method_recursion_depth_; detail::recursion_depth_guard guard{method_recursion_depth_};
using invocable_type = typename detail::filter_with<detail::batch_invocable, std::tuple<Components...>, Function>::type; using invocable_type = typename detail::filter_with<detail::batch_invocable, std::tuple<Components...>, Function>::type;
@ -710,8 +723,6 @@ namespace psemek::ecs
finalize_method(); finalize_method();
--method_recursion_depth_;
return cache; return cache;
} }

View file

@ -1,7 +1,7 @@
#pragma once #pragma once
#include <psemek/ecs/detail/stride.hpp> #include <psemek/ecs/detail/stride.hpp>
#include <psemek/ecs/detail/describe.hpp> #include <psemek/ecs/detail/to_string.hpp>
#include <psemek/util/uuid.hpp> #include <psemek/util/uuid.hpp>
#include <psemek/util/assert.hpp> #include <psemek/util/assert.hpp>
@ -33,6 +33,11 @@ namespace psemek::ecs::detail
return uuid_; return uuid_;
} }
std::string const & name() const
{
return name_;
}
std::type_index const & type() const std::type_index const & type() const
{ {
return type_; return type_;
@ -54,21 +59,20 @@ namespace psemek::ecs::detail
virtual std::size_t memory_usage() const = 0; virtual std::size_t memory_usage() const = 0;
virtual std::string describe() const = 0;
virtual std::string describe(std::uint32_t row) const = 0;
virtual ~column() = default; virtual ~column() = default;
protected: protected:
std::uint8_t * data_ = nullptr; std::uint8_t * data_ = nullptr;
std::size_t stride_; std::size_t stride_;
util::uuid uuid_; util::uuid uuid_;
std::string name_;
std::type_index type_; std::type_index type_;
bool copy_constructible_; bool copy_constructible_;
column(std::size_t stride, util::uuid const & uuid, std::type_index const & type, bool copy_constructible) column(std::size_t stride, util::uuid const & uuid, std::string name, std::type_index const & type, bool copy_constructible)
: stride_(stride) : stride_(stride)
, uuid_(uuid) , uuid_(uuid)
, name_(std::move(name))
, type_(type) , type_(type)
, copy_constructible_(copy_constructible) , copy_constructible_(copy_constructible)
{} {}
@ -91,9 +95,6 @@ namespace psemek::ecs::detail
std::size_t memory_usage() const override; std::size_t memory_usage() const override;
std::string describe() const override;
std::string describe(std::uint32_t row) const override;
~column_impl() override; ~column_impl() override;
private: private:
@ -120,15 +121,12 @@ namespace psemek::ecs::detail
std::size_t memory_usage() const override; std::size_t memory_usage() const override;
std::string describe() const override;
std::string describe(std::uint32_t row) const override;
~column_impl() override; ~column_impl() override;
}; };
template <typename Component, bool Empty> template <typename Component, bool Empty>
column_impl<Component, Empty>::column_impl() column_impl<Component, Empty>::column_impl()
: column(detail::stride<Component>(), Component::uuid(), typeid(Component), std::is_copy_constructible_v<Component>) : column(detail::stride<Component>(), Component::uuid(), component_name<Component>(), typeid(Component), std::is_copy_constructible_v<Component>)
{} {}
template <typename Component, bool Empty> template <typename Component, bool Empty>
@ -218,18 +216,6 @@ namespace psemek::ecs::detail
return sizeof(Component) * row_count_; return sizeof(Component) * row_count_;
} }
template <typename Component, bool Empty>
std::string column_impl<Component, Empty>::describe() const
{
return detail::describe<Component>();
}
template <typename Component, bool Empty>
std::string column_impl<Component, Empty>::describe(std::uint32_t row) const
{
return detail::describe(reinterpret_cast<Component const *>(data_)[row]);
}
template <typename Component, bool Empty> template <typename Component, bool Empty>
column_impl<Component, Empty>::~column_impl() column_impl<Component, Empty>::~column_impl()
{ {
@ -267,7 +253,7 @@ namespace psemek::ecs::detail
template <typename Component> template <typename Component>
column_impl<Component, true>::column_impl() column_impl<Component, true>::column_impl()
: column(detail::stride<Component>(), Component::uuid(), typeid(Component), std::is_copy_constructible_v<Component>) : column(detail::stride<Component>(), Component::uuid(), component_name<Component>(), typeid(Component), std::is_copy_constructible_v<Component>)
{ {
data_ = reinterpret_cast<std::uint8_t *>(new Component[1]); data_ = reinterpret_cast<std::uint8_t *>(new Component[1]);
} }
@ -308,18 +294,6 @@ namespace psemek::ecs::detail
return sizeof(Component); return sizeof(Component);
} }
template <typename Component>
std::string column_impl<Component, true>::describe() const
{
return detail::describe<Component>();
}
template <typename Component>
std::string column_impl<Component, true>::describe(std::uint32_t) const
{
return detail::describe(*reinterpret_cast<Component const *>(data_));
}
template <typename Component> template <typename Component>
column_impl<Component, true>::~column_impl() column_impl<Component, true>::~column_impl()
{ {

View file

@ -1,6 +1,7 @@
#pragma once #pragma once
#include <psemek/ecs/detail/column.hpp> #include <psemek/ecs/detail/column.hpp>
#include <psemek/ecs/detail/to_string.hpp>
#include <psemek/util/uuid.hpp> #include <psemek/util/uuid.hpp>
#include <psemek/util/hash_table.hpp> #include <psemek/util/hash_table.hpp>
#include <psemek/util/function.hpp> #include <psemek/util/function.hpp>
@ -32,36 +33,54 @@ namespace psemek::ecs::detail
struct component_registry struct component_registry
{ {
struct component_info
{
std::string name;
std::type_info const * type_info;
util::function<std::unique_ptr<column>()> column_factory;
util::function<std::string(void const *)> to_string;
};
template <typename Component> template <typename Component>
void register_component() void register_component()
{ {
auto const & uuid = Component::uuid(); auto const & uuid = Component::uuid();
if (auto it = types_.find(uuid); it != types_.end()) if (auto it = components_.find(uuid); it != components_.end())
{ {
if (it->second != &typeid(Component)) if (it->second.type_info != &typeid(Component))
throw duplicate_uuid_exception(uuid, *(it->second), typeid(Component)); throw duplicate_uuid_exception(uuid, *(it->second.type_info), typeid(Component));
else else
return; return;
} }
types_.insert({uuid, &typeid(Component)}); component_info & info = components_[uuid];
info.name = component_name<Component>();
column_factories_.insert({uuid, []{ info.type_info = &typeid(Component);
info.column_factory = []{
return std::make_unique<column_impl<Component>>(); return std::make_unique<column_impl<Component>>();
}}); };
info.to_string = [](void const * data){
return component_to_string(*static_cast<Component const *>(data));
};
} }
std::unique_ptr<column> create_column(util::uuid const & uuid) const std::unique_ptr<column> create_column(util::uuid const & uuid) const
{ {
if (auto it = column_factories_.find(uuid); it != column_factories_.end()) if (auto it = components_.find(uuid); it != components_.end())
return it->second(); return it->second.column_factory();
return nullptr;
}
component_info const * get_info(util::uuid const & uuid) const
{
if (auto it = components_.find(uuid); it != components_.end())
return &it->second;
return nullptr; return nullptr;
} }
private: private:
util::hash_map<util::uuid, std::type_info const *> types_; util::hash_map<util::uuid, component_info> components_;
util::hash_map<util::uuid, util::function<std::unique_ptr<column>()>> column_factories_;
}; };
} }

View file

@ -1,39 +0,0 @@
#pragma once
#include <psemek/util/type_name.hpp>
#include <string>
namespace psemek::ecs::detail
{
template <typename Component>
std::string describe()
{
if constexpr (requires {Component::to_string();})
{
return Component::to_string();
}
else
{
auto name = util::type_name<Component>();
if (auto pos = name.find_last_of(':'); pos != std::string::npos)
name = name.substr(pos + 1);
return name;
}
}
template <typename Component>
std::string describe(Component const & component)
{
if constexpr (requires {component.to_string();})
{
return component.to_string();
}
else
{
return describe<Component>();
}
}
}

View file

@ -0,0 +1,55 @@
#pragma once
#include <cstddef>
#include <utility>
namespace psemek::ecs::detail
{
struct recursion_depth_guard
{
recursion_depth_guard(std::size_t & value)
: depth_(&value)
{
++value;
}
recursion_depth_guard(recursion_depth_guard && other)
: depth_(other.depth_)
{
other.depth_ = nullptr;
}
recursion_depth_guard & operator = (recursion_depth_guard && other)
{
if (this != &other)
{
reset();
depth_ = other.release();
}
return *this;
}
~recursion_depth_guard()
{
reset();
}
void reset()
{
if (depth_)
--*depth_;
depth_ = nullptr;
}
std::size_t * release()
{
return std::exchange(depth_, nullptr);
}
private:
std::size_t * depth_;
};
}

View file

@ -11,7 +11,6 @@
#include <memory> #include <memory>
#include <vector> #include <vector>
#include <optional> #include <optional>
#include <string>
namespace psemek::ecs::detail namespace psemek::ecs::detail
{ {
@ -81,8 +80,7 @@ namespace psemek::ecs::detail
std::size_t memory_usage() const; std::size_t memory_usage() const;
std::string describe() const; std::string describe_short() const;
std::string describe(std::uint32_t row) const;
protected: protected:
std::uint64_t hash_; std::uint64_t hash_;

View file

@ -0,0 +1,43 @@
#pragma once
#include <psemek/util/type_name.hpp>
#include <string>
namespace psemek::ecs::detail
{
template <typename Component>
std::string component_name()
{
if constexpr (requires {Component::name();})
{
return Component::name();
}
else
{
static std::string const result = []{
std::string name = util::type_name<Component>();
if (auto pos = name.find_last_of(':'); pos != std::string::npos)
name = name.substr(pos + 1);
return name;
}();
return result;
}
}
template <typename Component>
std::string component_to_string(Component const & component)
{
if constexpr (requires {component.to_string();})
{
return component.to_string();
}
else
{
return "";
}
}
}

View file

@ -0,0 +1,19 @@
#pragma once
#include <psemek/ecs/detail/component_registry.hpp>
namespace psemek::ecs
{
struct entity_inspection_info
{
struct component_inspection_info
{
util::uuid uuid;
detail::component_registry::component_info const * info;
};
std::vector<component_inspection_info> components;
};
}

View file

@ -1,5 +1,6 @@
#include <psemek/ecs/container.hpp> #include <psemek/ecs/container.hpp>
#include <psemek/util/assert.hpp> #include <psemek/util/assert.hpp>
#include <psemek/util/string.hpp>
#include <algorithm> #include <algorithm>
@ -52,11 +53,47 @@ namespace psemek::ecs
return {data.table, data.row}; return {data.table, data.row};
} }
std::string container::describe(handle entity) const entity_inspection_info container::inspect(handle entity) const
{ {
assert(alive(entity)); entity_inspection_info result;
auto const data = entity_list_.get_entities()[entity.id]; auto const data = entity_list_.get_entities()[entity.id];
return data.table->describe(data.row); for (auto const & column : data.table->columns())
{
auto const uuid = column->uuid();
result.components.push_back({uuid, component_registry_.get_info(uuid)});
}
return result;
}
std::string container::describe_short(handle entity) const
{
return entity_list_.get_entities()[entity.id].table->describe_short();
}
std::string container::describe_detailed(handle entity) const
{
static std::string const indent = " ";
std::string result;
auto const data = entity_list_.get_entities()[entity.id];
for (auto const & column : data.table->columns())
{
auto const info = component_registry_.get_info(column->uuid());
result += info->name;
result += "\n";
auto const ptr = column->data() + column->stride() * data.row;
auto value = info->to_string(ptr);
if (!value.empty())
{
result += indent;
result += util::replace_all(util::trim(std::move(value)), "\n", "\n" + indent);
result += "\n";
}
}
return util::trim(result);
} }
bool container::can_clone(handle entity) const bool container::can_clone(handle entity) const
@ -190,7 +227,7 @@ namespace psemek::ecs
table_container_.apply([&](detail::table & table) table_container_.apply([&](detail::table & table)
{ {
result.tables.push_back({table.describe(), table.row_count()}); result.tables.push_back({table.describe_short(), table.row_count()});
}, {}, {}); }, {}, {});
return result; return result;

View file

@ -1,5 +1,6 @@
#include <psemek/ecs/detail/table.hpp> #include <psemek/ecs/detail/table.hpp>
#include <psemek/ecs/detail/unordered_component_hash.hpp> #include <psemek/ecs/detail/unordered_component_hash.hpp>
#include <psemek/ecs/detail/to_string.hpp>
#include <algorithm> #include <algorithm>
@ -190,26 +191,14 @@ namespace psemek::ecs::detail
return result; return result;
} }
std::string table::describe() const std::string table::describe_short() const
{ {
std::string result; std::string result;
for (std::size_t i = 0; i < columns_.size(); ++i) for (auto const & column : columns_)
{ {
if (i > 0) if (!result.empty())
result += ";"; result += ";";
result += columns_[i]->describe(); result += column->name();
}
return result;
}
std::string table::describe(std::uint32_t row) const
{
std::string result;
for (std::size_t i = 0; i < columns_.size(); ++i)
{
if (i > 0)
result += ";";
result += columns_[i]->describe(row);
} }
return result; return result;
} }

View file

@ -3,6 +3,7 @@
#include <psemek/gfx/array.hpp> #include <psemek/gfx/array.hpp>
#include <psemek/gfx/gl.hpp> #include <psemek/gfx/gl.hpp>
#include <psemek/util/string.hpp>
#include <psemek/util/to_string.hpp> #include <psemek/util/to_string.hpp>
#include <iomanip> #include <iomanip>
@ -67,17 +68,6 @@ void main()
} }
)"; )";
static void replace_all(std::string & str, std::string_view old_str, std::string_view new_str)
{
for (size_t i = 0;;)
{
i = str.find(old_str, i);
if (i == std::string::npos) break;
str.replace(i, old_str.size(), new_str);
i += new_str.size();
}
}
static std::string generate_blur_fs_source(int size, float sigma, bool horizontal) static std::string generate_blur_fs_source(int size, float sigma, bool horizontal)
{ {
std::string size_str = util::to_string(size); std::string size_str = util::to_string(size);
@ -105,9 +95,9 @@ void main()
} }
std::string result = blur_fs_template; std::string result = blur_fs_template;
replace_all(result, "@BLUR_SIZE@", size_str); result = util::replace_all(std::move(result), "@BLUR_SIZE@", size_str);
replace_all(result, "@BLUR_COEFFS@", coeffs_ss.str()); result = util::replace_all(std::move(result), "@BLUR_COEFFS@", coeffs_ss.str());
replace_all(result, "@BLUR_DIRECTION@", direction_str); result = util::replace_all(std::move(result), "@BLUR_DIRECTION@", direction_str);
return result; return result;
} }

View file

@ -0,0 +1,15 @@
#pragma once
#include <string>
#include <string_view>
namespace psemek::util
{
std::string replace_all(std::string str, std::string_view old_str, std::string_view new_str);
std::string trim_front(std::string str);
std::string trim_back(std::string str);
std::string trim(std::string str);
}

View file

@ -0,0 +1,42 @@
#include <psemek/util/string.hpp>
namespace psemek::util
{
std::string replace_all(std::string str, std::string_view old_str, std::string_view new_str)
{
for (size_t i = 0;;)
{
i = str.find(old_str, i);
if (i == std::string::npos) break;
str.replace(i, old_str.size(), new_str);
i += new_str.size();
}
return str;
}
static std::string const whitespace_mask = " \t\n\r\f\v";
std::string trim_front(std::string str)
{
str.erase(0, str.find_first_not_of(whitespace_mask));
return str;
}
std::string trim_back(std::string str)
{
size_t pos = str.find_last_not_of(whitespace_mask);
if (pos != std::string::npos) {
str.erase(pos + 1);
} else {
str.clear();
}
return str;
}
std::string trim(std::string str)
{
return trim_back(trim_front(std::move(str)));
}
}