From 65a69b1e3bd24636861181420f3e2ce8da0df28d Mon Sep 17 00:00:00 2001 From: Ezekiel Warren Date: Thu, 17 Apr 2025 12:23:01 -0700 Subject: [PATCH 1/3] feat: implement the new assoc index API --- README.md | 381 +++++++++++++++++++++++++++++ ecsact/entt/wrapper/dynamic.hh | 21 +- rt_entt_codegen/rt_entt_codegen.cc | 64 +---- test/assoc/BUILD.bazel | 3 + test/assoc/assoc.ecsact | 13 + test/assoc/assoc_test.cc | 30 +++ test/macros.bzl | 42 ++++ 7 files changed, 479 insertions(+), 75 deletions(-) create mode 100644 test/assoc/BUILD.bazel create mode 100644 test/assoc/assoc.ecsact create mode 100644 test/assoc/assoc_test.cc create mode 100644 test/macros.bzl diff --git a/README.md b/README.md index 4ae61e1..6bbe973 100644 --- a/README.md +++ b/README.md @@ -37,3 +37,384 @@ entt::basic_view, entt::exclude_t, entt::exclude_t<>> ``` + +## Association with entity fields + +Ecsact association requires us to handle systems and entities a little differently. I will explain the differences here. + +* multiple views per system +* multiple internal components per components with association fields + +In the examples below we'll assume the Ecsact file below and the following variables: +* `r` refers to an `entt::registry` +* `basic_view` refers to `entt::baic_view` +* `get_t` refers to `entt::get_t` +* `assoc` refers to `ecsact::entt::assoc` + + +```ecsact +component Power { f32 value; } +component Attacker { entity target; } +component Health { f32 value; } + +system DoDamage { + readonly Power; + readonly Health; + readonly Attacker with target { + readwrite Health; + } +} +``` + +### Adding/Updating with entity fields + +When adding a component with associated fields we must also add internal components to 'group' or 'bucket' entities together based on the association. This means that whenever there is an add or update we're actually adding _multiple_ components under the hood. + +```cpp +auto add_component(auto entity, Attacker attacker) -> void { + // add original component + r.emplace(entity, attacker); + + // add internal component if we meet the DoDamage target association conditions + if(r.all_of(attacker.target)) { + r.emplace(attacker.target, assoc{}); + } +} + +auto update_component(auto entity, Attacker attacker) -> void { + auto before = r.get(entity); + r.emplace_or_replace(entity, attacker); + + // if the target (entity field) has changed we must remove the old internal + // association component and add it to our new target + if(before.target != attacker.target) { + r.erase>(before.target); + + // but we only do so if it has the target association conditions! + if(r.all_of(attacker.target)) { + r.emplace(attacker.target, assoc{}); + } + } +} +``` + +The adding/updating/removing Health also gets more complicated. + +```cpp +auto add_component(auto entity, Health health) -> void { + r.emplace(entity, health); + + for(auto attacker_entity : r.view()) { + auto attacker = r.get(); + if(attacker.target == entity) { + r.emplace(attacker_entity, assoc{}); + break; + } + } +} + + +auto remove_component(auto entity) -> void { + r.erase(entity); + + for(auto attacker_entity : r.view()) { + auto attacker = r.get(); + if(attacker.target == entity) { + r.erase>(attacker_entity); + break; + } + } +} +``` + +### Iteration + +```cpp +// EnTT views for DoDamage +basic_view, get_t, get_t> +basic_view, assoc> +``` + +Iteration for `DoDamage` is less straight forward as a regular system. Instead of iterating over 1 view we will be iterating over both and hopefully in an optimal succinct fashion. + + +The inefficient way first: +```cpp +auto main_view = r.view(); +for(auto entity : main_view) { + auto attacker = main_view.get(entity); + auto assoc_view = r.view>(); + for(auto assoc_entity : assoc_view) { + if(assoc_entity == attacker.entity) { + // We found the associated entity! Call the system implementation + system_impl(...); + break; + } + } +} +``` + +This clearly is unoptimal. For every entity iteration we iterate over a second view. Ideally we can iterate each one by one, side by side. + +```cpp +auto main_view = r.view(); +auto assoc_view = r.view>(); + +auto main_view_itr = main_view.begin(); +auto assoc_view_itr = assoc_view.begin(); + +for(;;) { + if(main_view_itr == main_view.end()) break; + if(assoc_view_itr == assoc_view.end()) break; + + auto main_entity = *main_view_itr; + auto assoc_entity = *assoc_view_itr; + + // Can we assume that the main entity and the assoc entity match? + system_impl(...); + + ++main_view_itr; + ++assoc_view_itr; +} +``` + +Is there a way to we assmume the view iteration of both the `main_view` and `assoc_view` can be aligned? I'm not sure. Possibly through sorting the views and adding some extra internal component to the `main_view` so that the `main_view` and `assoc_view` match in length. + +If the above is not possible we could add a little bit of checking like so: + +```cpp +auto main_view = r.view(); +auto assoc_view = r.view>(); + +auto main_view_itr = main_view.begin(); +auto assoc_view_itr = assoc_view.begin(); + +for(;;) { + if(main_view_itr == main_view.end()) break; + + auto main_entity = *main_view_itr; + auto attacker = main_view.get(main_entity); + + while(assoc_view_itr != assoc_view.end()) { + // keep iterating until we found our attacker target entity + if(*assoc_view_itr == attacker.target) break; + ++assoc_view_itr; + + // TODO: if we reach the end of the assoc_view_itr we have to restart and + // make sure we're iterating only up until where we started to prevent + // infinite loops. + // if(assoc_view_itr == assoc_view.end()) { restart! } + } + + if(assoc_view_itr == assoc_view.end()) break; + + auto assoc_entity = *assoc_view_itr; + system_impl(...); + ++main_view_itr; +} +``` + +This does introduce a second set of iteration but would atleast guarantee we're only running our system implementation on the associated pair of entities. + +## Association indexed fields + +Association with indexed fields is similar to the entity fields except additional storage for the same types must be created. + +TODO: write about this + +```ecsact +component OnFire; +component Health { f32 value; } +component GridCell { i32 x; i32 y; } +component WithinCell { + GridCell.x x; + GridCell.y y; +} + +// When an entity with 'OnFire' is in a cell do damage to all other entities in +// the same cell +system BurnEveryoneInCell { + include OnFire; + readonly WithinCell with x,y { + readwrite Health; + } +} +``` + +### Adding/Updating with indexed fields + +Adds and updates need to add to a _different_ EnTT storage than the default when using indexed fields. This means only slight changes need to be done to add/update/remove and in some cases requires the indexed fields to be passed in directly. See below: + +```cpp +auto add_component(auto entity, GridCell grid_cell) -> void { + r.emplace(entity, grid_cell); // simple! + + // make sure the associated field storage is allocated + // this storage will be used for every `WithinCell` with the x/y value + // being the same as our `GridCell` + auto hash = storage_hash(grid_cell.x, grid_cell.y); + r.storage(hash); +} + +auto add_component(auto entity, WithinCell within_cell) -> void { + auto hash = storage_hash(within_cell.x, within_cell.y); + auto storage = r.storage(hash); + storage.push(entity, within_cell); // storage.push is the same as r.emplace +} + +auto update_component(auto entity, WithinCell within_cell, i32 x, i32 y) -> void { + // updating a component now requires the indexed fields to be passed in + // the reason being is we must remove the `WithinCell` from the storage of + // the previous indexed fields + + if(within_cell.x != x || within_cell.y != y) { + // delete from old storage + auto prev_hash = storage_hash(x, y); + auto prev_storage = r.storage(prev_hash); + prev_storage.erase(entity); + + // add to new storage + auto hash = storage_hash(within_cell.x, within_cell.y); + auto storage = r.storage(hash); + storage.push(entity, within_cell); + } else { + auto hash = storage_hash(within_cell.x, within_cell.y); + auto storage = r.storage(hash); + // update value! for `WithinCell` this wouldn't really happen because + // all of its fields as association fields, but in the case where a type + // had other fields this would be important + storage.get(entity) = within_cell; + } +} + +auto remove_component(auto entity, i32 x, i32 y) -> void { + // removing a component now also requires the indexed fields to be passed + // in-order to remove from the correct storage + auto hash = storage_hash(x, y); + auto storage = r.storage(hash); + storage.erase(entity); +} +``` + +You might have noticed that since we have different EnTT storage containers for the same component but with different values that you can have an entity with _multiple_ components of the same type. + +```cpp +auto entity = r.create(); +add_component(entity, WithinCell{0, 0}); // valid +add_component(entity, WithinCell{0, 1}); // also valid! +add_component(entity, WithinCell{2, -3}); // also valid! +``` + +This enables entities to be associated with multiple buckets. In a simple grid you could imagine that an entity with a large collision box would certainly be considered 'within' multiple cells. + +### Iteration + +Since we have unique hashed storage for the associated fields (`WithinCell` `x` and `y`) we can construct an `entt::runtime_view` with the storage based on our runtime value. Runtime views are more expensive than regular views, but it would be more expensive for us to check the matching value at runtime. + +```cpp +auto main_view = r.view(); +for(auto entity : main_view) { + auto on_fire_cell = main_view.get(entity); + auto within_cell_storage_hash = storage_hash(on_fire_cell.x, on_fire_cell.y); + + auto assoc_view = entt::runtime_view{}; + assoc_view.iterate(r.storage(within_cell_storage_hash)); + assoc_view.iterate(r.storage()); + + for(auto assoc_entity : assoc_view) { + system_impl(...); + } +} +``` + +...and I wish it was that simple. Unfortunately our `main_view` cannot simply use `WithinCell`'s default storage. We have a unique storage for every possible value of `WithinCell`'s indexed fields. Because of that we need to a way to retrieve all the possible storages and iterate over that. For that we create a storage for our storage. Its the storage storage. + +```cpp +template +struct storage_storage { + // storage hashes for type T + // NOTE: a vector on a component sounds kind of bad - not sure if there is + // a better way though + std::vector storage_hashes; + + auto add_hash(uint64_t) -> void; + auto remove_hash(uint64_t) -> void; +}; +``` + +Now in every add/update we must update the `storage_storage` for `WithinCell`. + +```cpp +auto add_component(auto entity, GridCell grid_cell) -> void { + // ... stuff before ... + + // nothing needs to change here +} + +auto add_component(auto entity, WithinCell within_cell) -> void { + // ... stuff before ... + + auto& storage_storage = r.emplace_or_replace>(entity); + auto hash = storage_hash(within_cell.x, within_cell.y); + storage_storage.storage_hashes.emplace_back(hash); +} + +auto update_component(auto entity, WithinCell within_cell, i32 x, i32 y) -> void { + // ... stuff before ... + + if(/* changed */) { + auto& storage_storage = r.emplace_or_replace>(entity); + auto prev_hash = storage_hash(x, y); + storage_storage.remove_hash(prev_hash); + + auto hash = storage_hash(within_cell.x, within_cell.y); + r.emplace(entity, storage_storage{hash}); + storage_storage.add_hash(hash); + } +} + +auto remove_component(auto entity, i32 x, i32 y) -> void { + // ... stuff before ... + + auto& storage_storage = r.emplace_or_replace>(entity); + auto hash = storage_hash(x, y); + storage_storage.remove_hash(hash); +} +``` + +Now with that our of the way we can introduce our `storage_storage` to our iteration. + + +```cpp +auto main_view = r.view>(); +for(auto entity : main_view) { + auto storage_storage = main_view.get>(entity); + for(auto hash : storage_storage.storage_hashes) { + auto within_cell_storage = r.storage(hash); + // NOTE: storage.get is much less efficient than a view.get + auto on_fire_cell = within_cell_storage.get(entity); + + for(auto entity : main_view) { + auto on_fire_cell = main_view.get(entity); + auto within_cell_storage_hash = storage_hash(on_fire_cell.x, on_fire_cell.y); + + auto assoc_view = entt::runtime_view{}; + assoc_view.iterate(r.storage(within_cell_storage_hash)); + assoc_view.iterate(r.storage()); + + for(auto assoc_entity : assoc_view) { + system_impl(...); + } + } + } +} +``` + +This introduces two slow downs: + +1) we're accessing the value of `WithinCell` in the main view with a `storage.get` instead of a `view.get` +2) we're doing an additional layer of iteration - this is only a minor drawback as the iteration count will generally be quite small + +## Combining Entity and Indexed fields strategy (the holy grail) + +TODO: write about this diff --git a/ecsact/entt/wrapper/dynamic.hh b/ecsact/entt/wrapper/dynamic.hh index 2242190..113a911 100644 --- a/ecsact/entt/wrapper/dynamic.hh +++ b/ecsact/entt/wrapper/dynamic.hh @@ -180,10 +180,9 @@ auto context_has( [[maybe_unused]] ecsact_component_like_id component_id, const void* indexed_fields ) -> bool { - static_assert( - !C::has_assoc_fields, - "Ecsact RT EnTT doesn't support indexed fields (yet)" - ); + if constexpr(C::has_assoc_fields) { + throw std::logic_error{"assoc context_has unimplemented"}; + } auto entity = context->entity; auto& registry = *context->registry; @@ -200,10 +199,9 @@ auto context_stream_toggle( ) -> void { using ecsact::entt::detail::run_on_stream; - static_assert( - !C::has_assoc_fields, - "Ecsact RT EnTT doesn't support indexed fields (yet)" - ); + if constexpr(C::has_assoc_fields) { + throw std::logic_error{"assoc stream_toggle unimplemented"}; + } auto entity = context->entity; auto& registry = *context->registry; @@ -229,10 +227,9 @@ auto context_generate_add( ) -> void { using ecsact::entt::detail::pending_add; - static_assert( - !C::has_assoc_fields, - "Ecsact RT EnTT doesn't support indexed fields (yet)" - ); + if constexpr(C::has_assoc_fields) { + throw std::logic_error{"assoc generate_add unimplemented"}; + } auto& registry = *context->registry; diff --git a/rt_entt_codegen/rt_entt_codegen.cc b/rt_entt_codegen/rt_entt_codegen.cc index 7bdb1c1..4956ac0 100644 --- a/rt_entt_codegen/rt_entt_codegen.cc +++ b/rt_entt_codegen/rt_entt_codegen.cc @@ -29,69 +29,7 @@ static auto check_unsupported_features( // ecsact::codegen_plugin_context& ctx, const ecsact::rt_entt_codegen::ecsact_entt_details& details ) -> bool { - auto found_assoc_feature = false; - for(auto comp_id : details.all_components) { - for(auto field_id : ecsact::meta::get_field_ids(comp_id)) { - auto field_type = ecsact::meta::get_field_type(comp_id, field_id); - if(field_type.kind == ECSACT_TYPE_KIND_BUILTIN && - field_type.type.builtin == ECSACT_ENTITY_TYPE) { - auto comp_name = ecsact::meta::decl_full_name(comp_id); - auto field_name = ecsact::meta::field_name(comp_id, field_id); - ctx.error("Assoc field found {}.{}", comp_name, field_name); - found_assoc_feature = true; - } else if(field_type.kind == ECSACT_TYPE_KIND_FIELD_INDEX) { - auto comp_name = ecsact::meta::decl_full_name(comp_id); - auto field_name = ecsact::meta::field_name(comp_id, field_id); - ctx.error("Assoc field found {}.{}", comp_name, field_name); - found_assoc_feature = true; - } - } - } - - for(auto sys_id : details.all_systems) { - auto assoc_ids = ecsact::meta::system_assoc_ids(sys_id); - if(!assoc_ids.empty()) { - found_assoc_feature = true; - auto system_name = ecsact::meta::decl_full_name(sys_id); - ctx.error("Assoc system found {}", system_name); - found_assoc_feature = true; - } - } - - for(auto act_id : details.all_actions) { - auto assoc_ids = ecsact::meta::system_assoc_ids(act_id); - if(!assoc_ids.empty()) { - found_assoc_feature = true; - auto system_name = ecsact::meta::decl_full_name(act_id); - ctx.error("Assoc action found {}", system_name); - found_assoc_feature = true; - } - - for(auto field_id : ecsact::meta::get_field_ids(act_id)) { - auto field_type = ecsact::meta::get_field_type(act_id, field_id); - if(field_type.kind == ECSACT_TYPE_KIND_BUILTIN && - field_type.type.builtin == ECSACT_ENTITY_TYPE) { - auto act_name = ecsact::meta::decl_full_name(act_id); - auto field_name = ecsact::meta::field_name(act_id, field_id); - ctx.error("Assoc field found {}.{}", act_name, field_name); - found_assoc_feature = true; - } else if(field_type.kind == ECSACT_TYPE_KIND_FIELD_INDEX) { - auto act_name = ecsact::meta::decl_full_name(act_id); - auto field_name = ecsact::meta::field_name(act_id, field_id); - ctx.error("Assoc field found {}.{}", act_name, field_name); - found_assoc_feature = true; - } - } - } - - if(found_assoc_feature) { - ctx.fatal( - "Association currently unsupported " - "https://github.com/ecsact-dev/ecsact_rt_entt/issues/138" - ); - return true; - } - + // we support it all! for now... return false; } diff --git a/test/assoc/BUILD.bazel b/test/assoc/BUILD.bazel new file mode 100644 index 0000000..ff2cd87 --- /dev/null +++ b/test/assoc/BUILD.bazel @@ -0,0 +1,3 @@ +load("//:macros.bzl", "rt_entt_test") + +rt_entt_test(name = "assoc") diff --git a/test/assoc/assoc.ecsact b/test/assoc/assoc.ecsact new file mode 100644 index 0000000..1913a93 --- /dev/null +++ b/test/assoc/assoc.ecsact @@ -0,0 +1,13 @@ +main package assoc_test; + +component Power { f32 value; } +component Attacker { entity target; } +component Health { f32 value; } + +system DoDamage { + readonly Power; + readonly Health; + readonly Attacker with target { + readwrite Health; + } +} diff --git a/test/assoc/assoc_test.cc b/test/assoc/assoc_test.cc new file mode 100644 index 0000000..01ced68 --- /dev/null +++ b/test/assoc/assoc_test.cc @@ -0,0 +1,30 @@ +#include "gtest/gtest.h" + +#include +#include "ecsact/runtime/core.hh" +#include "ecsact/runtime/dynamic.h" + +#include "assoc.ecsact.hh" +#include "assoc.ecsact.systems.hh" + +auto assoc_test::DoDamage::impl(context& ctx) -> void { + auto target_health = ctx.other().get(); + target_health.value -= ctx.get().value; + ctx.other().update(target_health); +} + +TEST(Assoc, AddComponentNoEntity) { + auto reg = ecsact::core::registry{"assoc_test_add_component"}; + auto attacker = reg.create_entity(); + + ASSERT_FALSE(ecsact_entity_exists(reg.id(), ecsact_entity_id{})); + + auto err = reg.add_component( + attacker, + assoc_test::Attacker{ + // invalid entity! doesn't exist! + .target = {}, + } + ); + EXPECT_EQ(err, ECSACT_ADD_ERR_ENTITY_INVALID); +} diff --git a/test/macros.bzl b/test/macros.bzl new file mode 100644 index 0000000..161176c --- /dev/null +++ b/test/macros.bzl @@ -0,0 +1,42 @@ +load("@ecsact_rt_entt//bazel:copts.bzl", "copts") +load("@ecsact_rt_entt//runtime:index.bzl", "ecsact_entt_runtime") +load("@rules_cc//cc:defs.bzl", "cc_test") +load("@rules_ecsact//ecsact:defs.bzl", "ecsact_codegen") + +def rt_entt_test(name = None): + ecsact_codegen( + name = "ecsact_cc_system_impl_srcs", + srcs = ["{}.ecsact".format(name)], + output_directory = "_ecsact_cc_system_impl_srcs", + plugins = [ + "@ecsact_lang_cpp//cpp_systems_source_codegen", + ], + ) + + ecsact_entt_runtime( + name = "{}_test_runtime".format(name), + srcs = ["{}.ecsact".format(name)], + ECSACT_ENTT_RUNTIME_PACKAGE = "::{}::package".format(name), + ECSACT_ENTT_RUNTIME_USER_HEADER = "{}.ecsact.meta.hh".format(name), + system_impls = ["dynamic"], + ) + + cc_test( + name = "{}_test".format(name), + srcs = [ + ":{}_test.cc".format(name), + ":ecsact_cc_system_impl_srcs", + ], + args = ["--gtest_catch_exceptions=0"], + copts = copts, + deps = [ + ":{}_test_runtime".format(name), + "@googletest//:gtest", + "@googletest//:gtest_main", + ], + ) + + native.alias( + name = name, + actual = ":{}_test".format(name), + ) From c74e68ab428a0e5269b371c4fb5fc5399c111d62 Mon Sep 17 00:00:00 2001 From: Ezekiel Warren Date: Fri, 18 Apr 2025 16:00:50 -0700 Subject: [PATCH 2/3] feat: sort of getting the storage stuff working --- MODULE.bazel | 10 +++ build_test.cc | 2 + ecsact/entt/detail/assoc.hh | 4 ++ ecsact/entt/detail/storage.hh | 120 +++++++++++++++++++++++++++++++++ ecsact/entt/wrapper/dynamic.hh | 86 +++++++++++------------ 5 files changed, 176 insertions(+), 46 deletions(-) create mode 100644 ecsact/entt/detail/assoc.hh create mode 100644 ecsact/entt/detail/storage.hh diff --git a/MODULE.bazel b/MODULE.bazel index 42ce843..ec9ee1b 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -41,3 +41,13 @@ register_toolchains( "@ecsact_toolchain//:all", dev_dependency = True, ) + +local_path_override( + module_name = "ecsact_lang_cpp", + path = "../ecsact_lang_cpp", +) + +local_path_override( + module_name = "ecsact_runtime", + path = "../ecsact_runtime", +) diff --git a/build_test.cc b/build_test.cc index aeb3e9f..e073e3a 100644 --- a/build_test.cc +++ b/build_test.cc @@ -9,6 +9,8 @@ #include "ecsact/entt/detail/registry.hh" // IWYU pragma: keep #include "ecsact/entt/detail/system_execution_context.hh" // IWYU pragma: keep #include "ecsact/entt/detail/apply_component_stream_data.hh" // IWYU pragma: keep +#include "ecsact/entt/detail/assoc.hh" // IWYU pragma: keep +#include "ecsact/entt/detail/storage.hh" // IWYU pragma: keep #include "ecsact/entt/entity.hh" // IWYU pragma: keep #include "ecsact/entt/error_check.hh" // IWYU pragma: keep #include "ecsact/entt/event_markers.hh" // IWYU pragma: keep diff --git a/ecsact/entt/detail/assoc.hh b/ecsact/entt/detail/assoc.hh new file mode 100644 index 0000000..6f3dac9 --- /dev/null +++ b/ecsact/entt/detail/assoc.hh @@ -0,0 +1,4 @@ +#pragma once + +#include "ecsact/entt/error_check.hh" +#include "entt/entity/registry.hpp" diff --git a/ecsact/entt/detail/storage.hh b/ecsact/entt/detail/storage.hh new file mode 100644 index 0000000..cbcc3ae --- /dev/null +++ b/ecsact/entt/detail/storage.hh @@ -0,0 +1,120 @@ +#pragma once + +#include +#include "ecsact/runtime/common.hh" +#include "ecsact/entt/entity.hh" +#include "ecsact/entt/detail/registry.hh" +#include "ecsact/entt/event_markers.hh" +#include "ecsact/entt/detail/internal_markers.hh" + +namespace ecsact::entt::detail { + +template +struct deferred_storage { + template typename WrapperT> + using storage_for = ::entt::storage_for_t>; + + ecsact::entt::registry_t& registry; + + auto add() -> storage_for; + auto remove() -> storage_for; +}; + +template +struct event_storage { + template typename WrapperT> + using storage_for = ::entt::storage_for_t>; + + ecsact::entt::registry_t& registry; + + auto removed() -> storage_for; + auto added() -> storage_for; + + auto beforeremove() -> storage_for; +}; + +struct storage { + template + using storage_for = ::entt::storage_for_t; + + ecsact::entt::registry_t& registry; + + /** + * Simply the container where an Ecsact component is stored + */ + template + auto component() -> storage_for; + + /** + * Containers responsible aiding with 'deferred' operations + */ + template + auto deferred() -> deferred_storage { + return deferred_storage{registry}; + } + + template + auto event() -> event_storage { + return event_storage{registry}; + } +}; + +/** + * Update component in storage with value if exists. If doesn't exist, add one + * with value. + */ +template + requires(!std::is_empty_v) +auto ensure_component( + ::entt::storage_for_t storage, + entity_id entity, + const C& value +) -> C& { +} + +template + requires(std::is_empty_v) +auto ensure_component( // + ::entt::storage_for_t storage, + entity_id entity +) -> void { +} + +template +auto remove_component( // + ::entt::storage_for_t storage, + entity_id entity +) -> void { +} + +template +auto remove_component_unchecked( + ::entt::storage_for_t storage, + entity_id entity +) -> void { +} + +template + requires(!std::is_empty_v) +auto add_component_unchecked( // + ::entt::storage_for_t storage, + entity_id entity +) -> void { +} + +template + requires(!std::is_empty_v) +auto add_component_unchecked( + ::entt::storage_for_t storage, + entity_id entity, + const C& component +) -> void { +} + +template +auto has_component( // + ::entt::storage_for_t storage, + entity_id entity +) -> bool { +} +} // namespace ecsact::entt::detail diff --git a/ecsact/entt/wrapper/dynamic.hh b/ecsact/entt/wrapper/dynamic.hh index 113a911..b692164 100644 --- a/ecsact/entt/wrapper/dynamic.hh +++ b/ecsact/entt/wrapper/dynamic.hh @@ -4,11 +4,14 @@ #include #include "ecsact/entt/entity.hh" #include "entt/entity/registry.hpp" +#include "ecsact/runtime/common.hh" #include "ecsact/entt/registry_util.hh" #include "ecsact/entt/error_check.hh" #include "ecsact/entt/detail/internal_markers.hh" #include "ecsact/entt/event_markers.hh" #include "ecsact/entt/detail/system_execution_context.hh" +#include "ecsact/entt/detail/assoc.hh" +#include "ecsact/entt/detail/storage.hh" #ifdef TRACY_ENABLE # include "tracy/Tracy.hpp" @@ -16,7 +19,7 @@ namespace ecsact::entt::wrapper::dynamic { -template +template auto context_add( ecsact_system_execution_context* context, [[maybe_unused]] ecsact_component_like_id component_id, @@ -36,19 +39,22 @@ auto context_add( auto entity = context->entity; auto& registry = *context->registry; - if constexpr(std::is_empty_v) { - registry.template emplace_or_replace>(entity); + auto s = detail::storage{registry}; + + if constexpr(ecsact::tag_component) { + detail::ensure_component(s.deferred().add(), entity); } else { const C* component = static_cast(component_data); - registry.template emplace_or_replace>(entity, *component); - registry.template remove>(entity); + detail::ensure_component(s.deferred().add(), entity, *component); + detail::remove_component(s.event().beforeremove(), entity); } if constexpr(!C::transient) { - if(registry.template all_of>(entity)) { - registry.template erase>(entity); + auto removed_event = s.event().removed(); + if(detail::has_component(removed_event, entity)) { + detail::remove_component_unchecked(removed_event, entity); } else { - registry.template emplace_or_replace>(entity); + detail::ensure_component(s.event().added()); } } } @@ -56,7 +62,7 @@ auto context_add( template auto component_add_trivial( ecsact::entt::registry_t& registry, - ecsact::entt::entity_id entity_id + ecsact::entt::entity_id entity ) -> void { #ifdef TRACY_ENABLE ZoneScopedC(tracy::Color::Teal); @@ -65,13 +71,16 @@ auto component_add_trivial( using ecsact::entt::component_removed; using ecsact::entt::detail::pending_add; - registry.template emplace_or_replace>(entity_id); + auto s = detail::storage{registry}; + + detail::ensure_component(s.deferred().add(), entity); if constexpr(!C::transient) { - if(registry.template all_of>(entity_id)) { - registry.template erase>(entity_id); + auto removed_event = s.event().removed(); + if(detail::has_component(removed_event, entity)) { + detail::remove_component_unchecked(removed_event, entity); } else { - registry.template emplace_or_replace>(entity_id); + detail::ensure_component(s.event().added()); } } } @@ -95,25 +104,22 @@ auto context_remove( auto entity = context->entity; auto& registry = *context->registry; - registry.template remove>(entity); - registry.template emplace_or_replace>(entity); - registry.template emplace_or_replace>(entity); - - // Stop here (tag) - if constexpr(!std::is_empty_v) { - auto component = view.template get(entity); + auto s = detail::storage{registry}; - auto& remove_storage = - registry.template emplace_or_replace>(entity); + detail::remove_component(s.event().added(), entity); + detail::ensure_component(s.deferred().remove(), entity); + detail::ensure_component(s.event().removed(), entity); - remove_storage.value = component; + if constexpr(!std::is_empty_v) { + const auto& component = view.template get(entity); + detail::ensure_component(s.event().beforeremove(), entity, component); } } template auto component_remove_trivial( ecsact::entt::registry_t& registry, - ecsact::entt::entity_id entity_id, + ecsact::entt::entity_id entity, auto& view ) -> void { #ifdef TRACY_ENABLE @@ -123,17 +129,15 @@ auto component_remove_trivial( using ecsact::entt::detail::beforeremove_storage; using ecsact::entt::detail::pending_remove; - registry.template remove>(entity_id); - registry.template emplace_or_replace>(entity_id); - registry.template emplace_or_replace>(entity_id); - - if constexpr(!std::is_empty_v) { - auto component = view.template get(entity_id); + auto s = detail::storage{registry}; - auto& remove_storage = - registry.template emplace_or_replace>(entity_id); + detail::remove_component(s.event().added(), entity); + detail::ensure_component(s.deferred().remove(), entity); + detail::ensure_component(s.event().removed(), entity); - remove_storage.value = component; + if constexpr(!std::is_empty_v) { + const auto& component = view.template get(entity); + detail::ensure_component(s.event().beforeremove(), entity, component); } } @@ -142,7 +146,6 @@ auto context_get( ecsact_system_execution_context* context, [[maybe_unused]] ecsact_component_like_id component_id, void* out_component_data, - const void* indexed_field_values, auto& view ) -> void { auto entity = context->entity; @@ -155,7 +158,6 @@ auto context_update( ecsact_system_execution_context* context, [[maybe_unused]] ecsact_component_like_id component_id, const void* in_component_data, - const void* indexed_field_values, auto& view ) -> void { using ecsact::entt::detail::exec_beforechange_storage; @@ -178,24 +180,16 @@ template auto context_has( ecsact_system_execution_context* context, [[maybe_unused]] ecsact_component_like_id component_id, - const void* indexed_fields + auto& view ) -> bool { - if constexpr(C::has_assoc_fields) { - throw std::logic_error{"assoc context_has unimplemented"}; - } - - auto entity = context->entity; - auto& registry = *context->registry; - - return registry.template any_of(entity); + return true; } template auto context_stream_toggle( ecsact_system_execution_context* context, [[maybe_unused]] ecsact_component_id component_id, - bool streaming_enabled, - const void* indexed_fields + bool streaming_enabled ) -> void { using ecsact::entt::detail::run_on_stream; From d7727b9652b60aa3ae9e74a3ed8594edcfbb0df3 Mon Sep 17 00:00:00 2001 From: Ezekiel Warren Date: Tue, 22 Apr 2025 12:38:29 -0700 Subject: [PATCH 3/3] feat: context generate add and stream market --- ecsact/entt/detail/storage.hh | 15 +++++++++++++++ ecsact/entt/wrapper/dynamic.hh | 16 ++++++++++------ 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/ecsact/entt/detail/storage.hh b/ecsact/entt/detail/storage.hh index cbcc3ae..7adcd60 100644 --- a/ecsact/entt/detail/storage.hh +++ b/ecsact/entt/detail/storage.hh @@ -33,6 +33,16 @@ struct event_storage { auto beforeremove() -> storage_for; }; +template +struct marker_storage { + template typename WrapperT> + using storage_for = ::entt::storage_for_t>; + + ecsact::entt::registry_t& registry; + + auto stream() -> storage_for; +}; + struct storage { template using storage_for = ::entt::storage_for_t; @@ -57,6 +67,11 @@ struct storage { auto event() -> event_storage { return event_storage{registry}; } + + template + auto marker() -> marker_storage { + return marker_storage{registry}; + } }; /** diff --git a/ecsact/entt/wrapper/dynamic.hh b/ecsact/entt/wrapper/dynamic.hh index b692164..dc50e22 100644 --- a/ecsact/entt/wrapper/dynamic.hh +++ b/ecsact/entt/wrapper/dynamic.hh @@ -199,14 +199,17 @@ auto context_stream_toggle( auto entity = context->entity; auto& registry = *context->registry; + auto s = detail::storage{registry}; + + auto stream_storage = s.marker().stream(); if(streaming_enabled) { - if(registry.any_of>(entity)) { - registry.template remove>(entity); + if(detail::has_component(stream_storage, entity)) { + detail::remove_component(stream_storage, entity); } } else { - if(!registry.any_of>(entity)) { - registry.template emplace>(entity); + if(!detail::has_component(stream_storage, entity)) { + detail::add_component_unchecked(stream_storage, entity); } } } @@ -226,10 +229,11 @@ auto context_generate_add( } auto& registry = *context->registry; + auto s = detail::storage{registry}; const auto& component = *static_cast(component_data); - registry.template emplace>(entity, component); - registry.template emplace_or_replace>(entity); + detail::add_component_unchecked(s.deferred().pending(), entity, component); + detail::ensure_component(s.event().added(), entity); } } // namespace ecsact::entt::wrapper::dynamic