diff --git a/docs/SAMPLES.md b/docs/SAMPLES.md index b4fe84e..55e7d8a 100644 --- a/docs/SAMPLES.md +++ b/docs/SAMPLES.md @@ -12,7 +12,7 @@ samples/ generate_replay.cpp vtx_sample_generate arena simulator -> 3 data sources advance_write.cpp vtx_sample_advance_write 3 data sources -> 3 .vtx replays (with post-processor) - arena_mappings.h JSON data model + JsonMapping + ArenaToVtx::MapFrame() + arena_mappings.h JSON data model + JsonMapping + StructMapping + StructFrameBinding arena_generated.h autogenerated schema constants + typed Views + Mutators + ForEachX helpers schemas/ arena_data.proto Protobuf game schema (namespace arena_pb) diff --git a/samples/advance_write.cpp b/samples/advance_write.cpp index 1080a90..4fd7ac7 100644 --- a/samples/advance_write.cpp +++ b/samples/advance_write.cpp @@ -18,8 +18,11 @@ // Mapping strategies demonstrated, one per format: // // JSON -> VTX::JsonMapping specializations in arena_mappings.h are -// walked by VTX::UniversalDeserializer to build ArenaReplayJson, -// then ArenaToVtx::MapFrame() produces each VTX::Frame. +// walked by VTX::UniversalDeserializer to build ArenaReplayJson. +// Then VTX::StructMapping + VTX::StructFrameBinding +// drive VTX::GenericNativeLoader::LoadFrame() to produce each +// VTX::Frame. Field addresses come from PropertyAddressCache, +// same as the FBS path below. // // Proto -> VTX::ProtoBinding specializations (below) are dispatched by // VTX::GenericProtobufLoader::LoadFrame(). Field-name lookups go @@ -43,6 +46,7 @@ #include "vtx/common/adapters/json/json_adapter.h" #include "vtx/common/vtx_types_helpers.h" #include "vtx/common/readers/frame_reader/flatbuffer_loader.h" +#include "vtx/common/readers/frame_reader/native_loader.h" #include "vtx/common/readers/frame_reader/protobuff_loader.h" #include "vtx/common/readers/frame_reader/universal_deserializer.h" #include "vtx/common/readers/schema_reader/schema_registry.h" @@ -164,8 +168,8 @@ namespace VTX { [](const ::arena_pb::Projectile& projectile) { return projectile.unique_id(); }); if (src.has_match_state()) { - loader.AppendSingleActor(entity_bucket, ArenaSchema::MatchState::StructName, src.match_state(), - [](const ::arena_pb::MatchState&) { return std::string("match_001"); }); + loader.AppendSingleEntity(entity_bucket, ArenaSchema::MatchState::StructName, src.match_state(), + [](const ::arena_pb::MatchState&) { return std::string("match_001"); }); } } }; @@ -276,13 +280,15 @@ namespace VTX { // Data source 1 -- JSON // =================================================================== // Parses the whole JSON blob on Initialize(), then cursors through the -// deserialized ArenaReplayJson frame-by-frame. The manual MapFrame() -// bridge from arena_mappings.h handles the arena-types -> VTX conversion. +// deserialized ArenaReplayJson frame-by-frame. GenericNativeLoader walks +// StructMapping<> / StructFrameBinding<> from arena_mappings.h to produce +// each VTX::Frame -- same PropertyAddressCache path as the FBS source. class ArenaJsonDataSource : public VTX::IFrameDataSource { public: - explicit ArenaJsonDataSource(std::string filepath) - : filepath_(std::move(filepath)) {} + ArenaJsonDataSource(std::string filepath, const VTX::PropertyAddressCache& cache) + : filepath_(std::move(filepath)) + , loader_(cache, false) {} bool Initialize() override { std::ifstream ifs(filepath_); @@ -310,7 +316,8 @@ class ArenaJsonDataSource : public VTX::IFrameDataSource { } const ArenaFrame& af = replay_.frames[cursor_++]; - out_frame = ArenaToVtx::MapFrame(af); + out_frame = VTX::Frame {}; + loader_.LoadFrame(af, out_frame, "ArenaFrame"); out_time = {af.game_time, std::nullopt, VTX::GameTime::EFilterType::OnlyIncreasing}; @@ -323,6 +330,7 @@ class ArenaJsonDataSource : public VTX::IFrameDataSource { std::string filepath_; nlohmann::json root_; ArenaReplayJson replay_; + VTX::GenericNativeLoader loader_; size_t total_ = 0; size_t cursor_ = 0; }; @@ -561,7 +569,7 @@ int main() { VTX_INFO("on every recorded frame; the persisted .vtx contains the post-processed values."); VTX_INFO("--- 1. JSON data source ---"); - ArenaJsonDataSource json_ds(writer_dir + "/arena_replay_data.json"); + ArenaJsonDataSource json_ds(writer_dir + "/arena_replay_data.json", arena_schema.GetPropertyCache()); RunPipeline(json_ds, reader_dir + "/arena_from_json_ds.vtx", schema, "arena-adv-json-0001", VTX::CreateFlatBuffersWriterFacade); diff --git a/samples/arena_mappings.h b/samples/arena_mappings.h index 8258255..18e37b7 100644 --- a/samples/arena_mappings.h +++ b/samples/arena_mappings.h @@ -1,55 +1,47 @@ #pragma once -// arena_mappings.h -- Arena game data model + JSON mapping for the samples. +// arena_mappings.h -- Arena game data model + mappings (JSON ingest + VTX slot output). // -// Two concerns live here: +// Two layers of compile-time reflection live here: // -// 1. ArenaVec3 / ArenaQuat / ArenaPlayer / ArenaProjectile / ArenaMatchState / -// ArenaFrame / ArenaReplayJson -// C++ types that mirror the arena replay JSON structure. +// 1. VTX::JsonMapping +// Drives UniversalDeserializer<>::Load(JsonAdapter) +// in advance_write.cpp. Walks JSON -> C++ struct. // -// 2. VTX::JsonMapping specializations for each of those types. -// Same compile-time reflection pattern used by real integrations -// (see tools/integrations/sf/sf_mappings.h). Consumed by -// VTX::UniversalDeserializer<>::Load(JsonAdapter) -// in advance_write.cpp. +// 2. VTX::StructMapping + VTX::StructFrameBinding +// Drives GenericNativeLoader::Load / LoadFrame in advance_write.cpp. +// Walks C++ struct -> VTX::PropertyContainer / VTX::Frame. // -// 3. ArenaToVtx::MapFrame() -// Hand-written bridge from the arena data model to a VTX::Frame -// (PropertyContainer layout matches content/writer/arena/arena_schema.json). -// This is the "manual" counterpart to the schema-driven ProtoBinding -// and FlatBufferBinding specializations declared in advance_write.cpp. -// -// Property-vector indices assigned here MUST match the field order in -// arena_schema.json -- see the comments above each MapXxx() function. +// The C++ data model uses VTX::Vector / VTX::Quat directly so no per-field +// conversion is needed between the JSON ingest layer and the VTX output layer. +// (This is the equivalent of the manual ArenaToVtx::Map* helpers we deleted -- +// the schema-driven StructMapping<> now does that work automatically.) #include #include +#include #include #include "vtx/common/adapters/json/json_policy.h" +#include "vtx/common/adapters/native/struct_mapping.h" +#include "vtx/common/readers/frame_reader/native_loader.h" #include "vtx/common/readers/frame_reader/type_traits.h" #include "vtx/common/vtx_types.h" +#include "arena_generated.h" + // =================================================================== // Arena game data model (matches the JSON data source structure) // =================================================================== -struct ArenaVec3 { - double x = 0, y = 0, z = 0; -}; -struct ArenaQuat { - float x = 0, y = 0, z = 0, w = 1; -}; - struct ArenaPlayer { std::string unique_id; std::string name; int team = 0; float health = 100.0f; float armor = 50.0f; - ArenaVec3 position; - ArenaQuat rotation; - ArenaVec3 velocity; + VTX::Vector position; + VTX::Quat rotation; + VTX::Vector velocity; bool is_alive = true; int score = 0; int deaths = 0; @@ -58,13 +50,16 @@ struct ArenaPlayer { struct ArenaProjectile { std::string unique_id; std::string owner_id; - ArenaVec3 position; - ArenaVec3 velocity; + VTX::Vector position; + VTX::Vector velocity; float damage = 25.0f; std::string type = "bullet"; }; struct ArenaMatchState { + // Default to the canonical id. The match-state JSON doesn't carry one; + // the FB/Proto bindings inject the same literal -- this keeps parity. + std::string unique_id = "match_001"; int score_team1 = 0; int score_team2 = 0; int round = 1; @@ -90,25 +85,24 @@ struct ArenaReplayJson { }; // =================================================================== -// JsonMapping specializations (JSON key → C++ member) -// -// Same pattern as sf_mappings.h. Consumed by UniversalDeserializer -// or any code that inspects the mapping tuple at compile time. +// JsonMapping specializations (JSON key -> C++ member) // =================================================================== +// Two new mappings for the VTX math types so the JSON ingest layer can fill +// them directly. Same shape as the JSON file: {"x":..., "y":..., "z":...}. template <> -struct VTX::JsonMapping { +struct VTX::JsonMapping { static constexpr auto GetFields() { - return std::make_tuple(MakeField("x", &ArenaVec3::x), MakeField("y", &ArenaVec3::y), - MakeField("z", &ArenaVec3::z)); + return std::make_tuple(MakeField("x", &VTX::Vector::x), MakeField("y", &VTX::Vector::y), + MakeField("z", &VTX::Vector::z)); } }; template <> -struct VTX::JsonMapping { +struct VTX::JsonMapping { static constexpr auto GetFields() { - return std::make_tuple(MakeField("x", &ArenaQuat::x), MakeField("y", &ArenaQuat::y), - MakeField("z", &ArenaQuat::z), MakeField("w", &ArenaQuat::w)); + return std::make_tuple(MakeField("x", &VTX::Quat::x), MakeField("y", &VTX::Quat::y), + MakeField("z", &VTX::Quat::z), MakeField("w", &VTX::Quat::w)); } }; @@ -137,6 +131,9 @@ struct VTX::JsonMapping { template <> struct VTX::JsonMapping { + // unique_id is intentionally NOT in this mapping -- it isn't in the JSON file. + // The C++ default ("match_001") survives because UniversalDeserializer + // skips missing keys. static constexpr auto GetFields() { return std::make_tuple(MakeField("score_team1", &ArenaMatchState::score_team1), MakeField("score_team2", &ArenaMatchState::score_team2), @@ -167,80 +164,78 @@ struct VTX::JsonMapping { }; // =================================================================== -// ArenaToVtx — arena game types → VTX PropertyContainer -// -// Property indices must match the field order in arena_schema.json. +// StructMapping specializations (C++ member -> VTX slot name) // =================================================================== +// Replaces the hand-written ArenaToVtx::MapPlayer / MapProjectile / +// MapMatchState helpers from the previous version. GenericNativeLoader +// walks these tuples automatically. -namespace ArenaToVtx { - - inline VTX::Vector ToVtxVector(const ArenaVec3& v) { - return {v.x, v.y, v.z}; - } - inline VTX::Quat ToVtxQuat(const ArenaQuat& q) { - return {q.x, q.y, q.z, q.w}; +template <> +struct VTX::StructMapping { + static constexpr auto GetFields() { + return std::make_tuple(MakeStructField(ArenaSchema::Player::UniqueID, &ArenaPlayer::unique_id), + MakeStructField(ArenaSchema::Player::Name, &ArenaPlayer::name), + MakeStructField(ArenaSchema::Player::Team, &ArenaPlayer::team), + MakeStructField(ArenaSchema::Player::Health, &ArenaPlayer::health), + MakeStructField(ArenaSchema::Player::Armor, &ArenaPlayer::armor), + MakeStructField(ArenaSchema::Player::Position, &ArenaPlayer::position), + MakeStructField(ArenaSchema::Player::Rotation, &ArenaPlayer::rotation), + MakeStructField(ArenaSchema::Player::Velocity, &ArenaPlayer::velocity), + MakeStructField(ArenaSchema::Player::IsAlive, &ArenaPlayer::is_alive), + MakeStructField(ArenaSchema::Player::Score, &ArenaPlayer::score), + MakeStructField(ArenaSchema::Player::Deaths, &ArenaPlayer::deaths)); } +}; - /// Player → entity_type_id 0 - /// string[0]=UniqueID string[1]=Name - /// int32[0]=Team int32[1]=Score int32[2]=Deaths - /// float[0]=Health float[1]=Armor - /// vector[0]=Position vector[1]=Velocity - /// quat[0]=Rotation - /// bool[0]=IsAlive - inline VTX::PropertyContainer MapPlayer(const ArenaPlayer& p) { - VTX::PropertyContainer pc; - pc.entity_type_id = 0; - pc.string_properties = {p.unique_id, p.name}; - pc.int32_properties = {p.team, p.score, p.deaths}; - pc.float_properties = {p.health, p.armor}; - pc.vector_properties = {ToVtxVector(p.position), ToVtxVector(p.velocity)}; - pc.quat_properties = {ToVtxQuat(p.rotation)}; - pc.bool_properties = {p.is_alive}; - return pc; +template <> +struct VTX::StructMapping { + static constexpr auto GetFields() { + return std::make_tuple(MakeStructField(ArenaSchema::Projectile::UniqueID, &ArenaProjectile::unique_id), + MakeStructField(ArenaSchema::Projectile::OwnerID, &ArenaProjectile::owner_id), + MakeStructField(ArenaSchema::Projectile::Position, &ArenaProjectile::position), + MakeStructField(ArenaSchema::Projectile::Velocity, &ArenaProjectile::velocity), + MakeStructField(ArenaSchema::Projectile::Damage, &ArenaProjectile::damage), + MakeStructField(ArenaSchema::Projectile::Type, &ArenaProjectile::type)); } +}; - /// Projectile → entity_type_id 1 - /// string[0]=UniqueID string[1]=OwnerID string[2]=Type - /// vector[0]=Position vector[1]=Velocity - /// float[0]=Damage - inline VTX::PropertyContainer MapProjectile(const ArenaProjectile& pr) { - VTX::PropertyContainer pc; - pc.entity_type_id = 1; - pc.string_properties = {pr.unique_id, pr.owner_id, pr.type}; - pc.vector_properties = {ToVtxVector(pr.position), ToVtxVector(pr.velocity)}; - pc.float_properties = {pr.damage}; - return pc; +template <> +struct VTX::StructMapping { + static constexpr auto GetFields() { + return std::make_tuple( + MakeStructField(ArenaSchema::MatchState::UniqueID, &ArenaMatchState::unique_id), + MakeStructField(ArenaSchema::MatchState::ScoreTeam1, &ArenaMatchState::score_team1), + MakeStructField(ArenaSchema::MatchState::ScoreTeam2, &ArenaMatchState::score_team2), + MakeStructField(ArenaSchema::MatchState::Round, &ArenaMatchState::round), + MakeStructField(ArenaSchema::MatchState::Phase, &ArenaMatchState::phase), + MakeStructField(ArenaSchema::MatchState::TimeRemaining, &ArenaMatchState::time_remaining)); } +}; - /// MatchState → entity_type_id 2 - /// string[0]=UniqueID string[1]=Phase - /// int32[0]=ScoreTeam1 int32[1]=ScoreTeam2 int32[2]=Round - /// float[0]=TimeRemaining - inline VTX::PropertyContainer MapMatchState(const ArenaMatchState& m) { - VTX::PropertyContainer pc; - pc.entity_type_id = 2; - pc.string_properties = {"match_001", m.phase}; - pc.int32_properties = {m.score_team1, m.score_team2, m.round}; - pc.float_properties = {m.time_remaining}; - return pc; - } +// =================================================================== +// StructFrameBinding (ArenaFrame -> VTX::Frame buckets) +// =================================================================== +// Same pattern as FlatBufferBinding::TransferToFrame +// and ProtoBinding::TransferToFrame in advance_write.cpp. +// The bucket/entity layout is game-specific; what gets pushed into each +// PropertyContainer is driven automatically by StructMapping<> above. - /// Full frame → single "entity" bucket. - inline VTX::Frame MapFrame(const ArenaFrame& af) { - VTX::Frame frame; - VTX::Bucket& bucket = frame.CreateBucket("entity"); - for (const auto& p : af.players) { - bucket.unique_ids.push_back(p.unique_id); - bucket.entities.push_back(MapPlayer(p)); - } - for (const auto& pr : af.projectiles) { - bucket.unique_ids.push_back(pr.unique_id); - bucket.entities.push_back(MapProjectile(pr)); - } - bucket.unique_ids.push_back("match_001"); - bucket.entities.push_back(MapMatchState(af.match_state)); - return frame; +template <> +struct VTX::StructFrameBinding { + static void TransferToFrame(const ArenaFrame& src, VTX::Frame& dest, VTX::GenericNativeLoader& loader, + const std::string& /*schema_name*/) { + dest = VTX::Frame {}; + VTX::Bucket& bucket = dest.GetBucket("entity"); + bucket.entities.clear(); + bucket.unique_ids.clear(); + + loader.AppendActorList(bucket, VTX::ArenaSchema::Player::StructName, src.players, + [](const ArenaPlayer& p) { return p.unique_id; }); + + loader.AppendActorList(bucket, VTX::ArenaSchema::Projectile::StructName, src.projectiles, + [](const ArenaProjectile& p) { return p.unique_id; }); + + loader.AppendSingleEntity(bucket, VTX::ArenaSchema::MatchState::StructName, src.match_state, + [](const ArenaMatchState& m) { return m.unique_id; }); } - -} // namespace ArenaToVtx +}; diff --git a/samples/content/reader/arena/arena_from_fbs_ds.vtx b/samples/content/reader/arena/arena_from_fbs_ds.vtx index fad0da8..d57d614 100644 Binary files a/samples/content/reader/arena/arena_from_fbs_ds.vtx and b/samples/content/reader/arena/arena_from_fbs_ds.vtx differ diff --git a/samples/content/reader/arena/arena_from_json_ds.vtx b/samples/content/reader/arena/arena_from_json_ds.vtx index fc7e6ad..6ec23f6 100644 Binary files a/samples/content/reader/arena/arena_from_json_ds.vtx and b/samples/content/reader/arena/arena_from_json_ds.vtx differ diff --git a/samples/content/reader/arena/arena_from_proto_ds.vtx b/samples/content/reader/arena/arena_from_proto_ds.vtx index a8361a5..f26e875 100644 Binary files a/samples/content/reader/arena/arena_from_proto_ds.vtx and b/samples/content/reader/arena/arena_from_proto_ds.vtx differ diff --git a/sdk/include/vtx/common/adapters/native/struct_mapping.h b/sdk/include/vtx/common/adapters/native/struct_mapping.h new file mode 100644 index 0000000..c3a15a6 --- /dev/null +++ b/sdk/include/vtx/common/adapters/native/struct_mapping.h @@ -0,0 +1,98 @@ +/** +* @file struct_mapping.h + * @brief Compile-time reflection for "C++ struct -> VTX slots" mappings. + * + * Twin of JsonMapping but the key is the VTX slot name (not a JSON key). + * Consumed by GenericNativeLoader to walk a C++ instance and push each member + * into the right PropertyContainer slot via the shared base's LoadField. + * + * Usage: + * + * template<> + * struct VTX::StructMapping { + * static constexpr auto GetFields() { + * return std::make_tuple( + * MakeStructField(ArenaSchema::Player::UniqueID, &ArenaPlayer::unique_id), + * MakeStructField(ArenaSchema::Player::Health, &ArenaPlayer::health), + * MakeStructField(ArenaSchema::Player::Position, &ArenaPlayer::position), + * ...); + * } + * }; + * + * @author Zenos Interactive + */ + +#pragma once +#include + +namespace VTX { + + /** + * @brief A single mapped field linking a VTX slot name to a C++ member. + * @tparam Class The struct that owns the member. + * @tparam Type The member's type. + */ + template + struct StructField { + const char* name; + Type Class::*member_ptr; + }; + + + /** + * @brief Helper to build a StructField with template deduction. + * Usage: MakeStructField("Health", &Player::health) + */ + template + constexpr auto MakeStructField(const char* name, Type Class::*member_ptr) { + return StructField {name, member_ptr}; + } + + /** + * @brief Mapping trait. Users specialize for their types. + * @details The specialization must provide a static `GetFields()` method + * returning a tuple of StructField objects. + */ + template + struct StructMapping; + + /** + * @brief Frame-level binding trait. Users specialize with a `TransferToFrame` + * static method that builds VTX::Frame buckets from the source struct. + * + * Equivalent to FlatBufferBinding::TransferToFrame and + * ProtoBinding::TransferToFrame. + * + * Usage: + * template<> + * struct VTX::StructFrameBinding { + * static void TransferToFrame(const ArenaFrame& src, VTX::Frame& dest, + * GenericNativeLoader& loader, + * const std::string& schema_name) { + * auto& bucket = dest.GetBucket("entity"); + * loader.AppendActorList(bucket, ArenaSchema::Player::StructName, + * src.players, + * [](const ArenaPlayer& p){ return p.unique_id; }); + * ... + * } + * }; + */ + template + struct StructFrameBinding; + + template + struct HasStructMapping : std::false_type {}; + + template + struct HasStructMapping::GetFields())>> : std::true_type {}; + + /** + * @brief True if T has a registered StructMapping specialization. + * Used by GenericNativeLoader to decide between recursive Load (struct mapped) + * and direct LoadField / FillFlatArray (primitive or VTX-native type). + */ + template + inline constexpr bool has_struct_mapping_v = HasStructMapping::value; + + +} // namespace VTX diff --git a/sdk/include/vtx/common/readers/frame_reader/flatbuffer_loader.h b/sdk/include/vtx/common/readers/frame_reader/flatbuffer_loader.h index d74187c..a17e7fb 100644 --- a/sdk/include/vtx/common/readers/frame_reader/flatbuffer_loader.h +++ b/sdk/include/vtx/common/readers/frame_reader/flatbuffer_loader.h @@ -1,32 +1,52 @@ /** * @file flatbuffer_loader.h - * @brief Provides an ULTRA-FAST generic loading mechanism from FlatBuffers to VTX native structures using PropertyAddressCache. + * @brief Generic FlatBuffers -> VTX::PropertyContainer loader, on top of GenericLoaderBase. + * + * Inherits LoadField / LoadBlob / AppendActorList / AppendSingleEntity / + * StoreValue / EnsureSize / PushToFlatArray / FillFlatArray from the CRTP base. + * Implements the format-specific bits: ResolveField (cache lookup) and the + * pointer-based Load / LoadStruct / LoadArray that walk FlatBuffer tables. + * + * @author Zenos Interactive */ #pragma once -#include -#include -#include + +#include "vtx/common/readers/frame_reader/loader_base.h" #include "vtx/common/vtx_property_cache.h" #include "vtx/common/vtx_types.h" +#include "vtx/common/vtx_types_helpers.h" + +#include +#include +#include namespace VTX { template struct FlatBufferBinding { - static_assert(sizeof(T) == 0, "ERROR: Missing Bindings for this FlatBuffer type."); + static_assert(sizeof(T) == 0, "ERROR: Missing FlatBufferBinding specialization."); }; - class GenericFlatBufferLoader { - private: - const PropertyAddressCache* cache_; - bool debug_mode_; - + class GenericFlatBufferLoader : public GenericLoaderBase { public: explicit GenericFlatBufferLoader(const PropertyAddressCache& cache, bool debug = false) : cache_(&cache) , debug_mode_(debug) {} + + const PropertyAddress* ResolveField(int32_t entity_type_id, const std::string& /*struct_name*/, + const std::string& field_name) const { + auto struct_it = cache_->structs.find(entity_type_id); + if (struct_it == cache_->structs.end()) + return nullptr; + auto prop_it = struct_it->second.properties.find(field_name); + if (prop_it == struct_it->second.properties.end()) + return nullptr; + return &prop_it->second; + } + + template void Load(const FBType* src, PropertyContainer& dest, const std::string& struct_name) { if (!src) @@ -44,147 +64,56 @@ namespace VTX { } template - void LoadFrame(const FrameFBType* src, VTX::Frame& dest, const std::string& schemaName) { + void LoadFrame(const FrameFBType* src, VTX::Frame& dest, const std::string& schema_name) { if (!src) return; - FlatBufferBinding::TransferToFrame(src, dest, *this, schemaName); + FlatBufferBinding::TransferToFrame(src, dest, *this, schema_name); } - template - void AppendActorList(VTX::Bucket& targetBlock, const std::string& schemaType, const FBVectorType* src_vector, - IdExtractorFunc idExtractor) { + + template + void AppendActorList(VTX::Bucket& bucket, const std::string& schema_type, const FBVectorType* src_vector, + IdFunc id_func) { if (!src_vector || src_vector->size() == 0) return; - targetBlock.entities.reserve(targetBlock.entities.size() + src_vector->size()); - - for (auto it = src_vector->begin(); it != src_vector->end(); ++it) { - ExtractActorWithIdFunc(*it, targetBlock, schemaType, idExtractor); - } + GenericLoaderBase::AppendActorList(bucket, schema_type, *src_vector, id_func); } - template - void AppendSingleEntity(VTX::Bucket& targetBlock, const std::string& schemaType, const FBType* src_item, - IdExtractorFunc idExtractor) { + template + void AppendSingleEntity(VTX::Bucket& bucket, const std::string& schema_type, const FBType* src_item, + IdFunc id_func) { if (!src_item) return; - ExtractActorWithIdFunc(src_item, targetBlock, schemaType, idExtractor); - } - - template - void LoadField(PropertyContainer& dest, const std::string& /*struct_name*/, const std::string& field_name, - const T& value) { - auto struct_it = cache_->structs.find(dest.entity_type_id); - if (struct_it == cache_->structs.end()) - return; - - auto prop_it = struct_it->second.properties.find(field_name); - if (prop_it == struct_it->second.properties.end()) - return; - - const PropertyAddress& addr = prop_it->second; - - switch (addr.type_id) { - case FieldType::String: - StoreValue(dest.string_properties, addr.index, value); - break; - case FieldType::Int8: - case FieldType::Int32: - case FieldType::Enum: - StoreValue(dest.int32_properties, addr.index, value); - break; - case FieldType::Int64: - StoreValue(dest.int64_properties, addr.index, value); - break; - case FieldType::Float: - StoreValue(dest.float_properties, addr.index, value); - break; - case FieldType::Double: - StoreValue(dest.double_properties, addr.index, value); - break; - case FieldType::Bool: - StoreValue(dest.bool_properties, addr.index, value); - break; - case FieldType::Vector: - StoreValue(dest.vector_properties, addr.index, value); - break; - case FieldType::Quat: - StoreValue(dest.quat_properties, addr.index, value); - break; - case FieldType::Transform: - StoreValue(dest.transform_properties, addr.index, value); - break; - case FieldType::FloatRange: - StoreValue(dest.range_properties, addr.index, value); - break; - case FieldType::Struct: - StoreValue(dest.any_struct_properties, addr.index, value); - break; - default: - break; - } + GenericLoaderBase::AppendSingleEntity(bucket, schema_type, src_item, id_func); } - void LoadBlob(PropertyContainer& dest, const std::string& /*struct_name*/, const std::string& field_name, - const void* data, size_t byte_size) { - if (!data || byte_size == 0) - return; - - auto struct_it = cache_->structs.find(dest.entity_type_id); - if (struct_it == cache_->structs.end()) - return; - - auto prop_it = struct_it->second.properties.find(field_name); - if (prop_it == struct_it->second.properties.end()) - return; - - int32_t idx = prop_it->second.index; - const uint8_t* byte_data = static_cast(data); - - for (size_t i = 0; i < byte_size; ++i) { - dest.byte_array_properties.PushBack(idx, byte_data[i]); - } - } template - void LoadStruct(PropertyContainer& dest, const std::string& /*struct_name*/, const std::string& field_name, + void LoadStruct(PropertyContainer& dest, const std::string& struct_name, const std::string& field_name, const NestedFBType* src_nested) { if (!src_nested) return; - - auto struct_it = cache_->structs.find(dest.entity_type_id); - if (struct_it == cache_->structs.end()) - return; - - auto prop_it = struct_it->second.properties.find(field_name); - if (prop_it == struct_it->second.properties.end()) + const auto* addr = ResolveField(dest.entity_type_id, struct_name, field_name); + if (!addr) return; - int32_t index = prop_it->second.index; - const std::string& child_schema = prop_it->second.child_type_name; - - EnsureSize(dest.any_struct_properties, index); - Load(src_nested, dest.any_struct_properties[index], child_schema); + this->EnsureSize(dest.any_struct_properties, addr->index); + Load(src_nested, dest.any_struct_properties[addr->index], addr->child_type_name); } template - void LoadArray(PropertyContainer& dest, const std::string& /*struct_name*/, const std::string& field_name, + void LoadArray(PropertyContainer& dest, const std::string& struct_name, const std::string& field_name, const FBVectorType* src_array) { if (!src_array || src_array->size() == 0) return; - - auto struct_it = cache_->structs.find(dest.entity_type_id); - if (struct_it == cache_->structs.end()) - return; - - auto prop_it = struct_it->second.properties.find(field_name); - if (prop_it == struct_it->second.properties.end()) + const auto* addr = ResolveField(dest.entity_type_id, struct_name, field_name); + if (!addr) return; - const int32_t idx = prop_it->second.index; - const FieldType type_id = prop_it->second.type_id; - - const std::string& child_schema = prop_it->second.child_type_name; - const VTX::FieldContainerType container = prop_it->second.container_type; + const int32_t idx = addr->index; + const FieldType type_id = addr->type_id; + const std::string& child_schema = addr->child_type_name; + const FieldContainerType container = addr->container_type; using IteratorT = typename FBVectorType::const_iterator; using ElementT = typename std::iterator_traits::value_type; @@ -200,8 +129,8 @@ namespace VTX { Load(item, nested_container, child_schema); if (nested_container.entity_type_id != -1) { - if (container == VTX::FieldContainerType::Map) { - EnsureSize(dest.map_properties, idx); + if (container == FieldContainerType::Map) { + this->EnsureSize(dest.map_properties, idx); std::string map_key; if (!nested_container.string_properties.empty() && @@ -215,7 +144,6 @@ namespace VTX { dest.map_properties[idx].keys.push_back(map_key); dest.map_properties[idx].values.push_back(nested_container); - } else { dest.any_struct_arrays.PushBack(idx, nested_container); } @@ -223,105 +151,17 @@ namespace VTX { } else { PropertyContainer temp; Load(item, temp, child_schema); - PushToFlatArray(dest, type_id, idx, temp); + this->PushToFlatArray(dest, type_id, idx, temp); } } } else { - FillFlatArray(dest, type_id, idx, src_array); + this->FillFlatArray(dest, type_id, idx, *src_array); } } private: - template - void FillFlatArray(PropertyContainer& dest, FieldType type, int32_t idx, const FBVectorT* src) { - if (!src) - return; - for (auto it = src->begin(); it != src->end(); ++it) { - const auto& val = *it; - switch (type) { - case FieldType::Int8: - case FieldType::Int32: - case FieldType::Enum: - dest.int32_arrays.PushBack(idx, static_cast(val)); - break; - case FieldType::Int64: - dest.int64_arrays.PushBack(idx, static_cast(val)); - break; - case FieldType::Float: - dest.float_arrays.PushBack(idx, static_cast(val)); - break; - case FieldType::Double: - dest.double_arrays.PushBack(idx, static_cast(val)); - break; - case FieldType::Bool: - dest.bool_arrays.PushBack(idx, static_cast(val)); - break; - case FieldType::String: - if constexpr (std::is_assignable_v) { - dest.string_arrays.PushBack(idx, val); - } else { - dest.string_arrays.PushBack(idx, std::to_string(val)); - } - break; - default: - break; - } - } - } - - void PushToFlatArray(PropertyContainer& dest, FieldType type, int32_t idx, - const PropertyContainer& temp) const { - switch (type) { - case FieldType::Vector: - if (!temp.vector_properties.empty()) - dest.vector_arrays.PushBack(idx, temp.vector_properties[0]); - break; - case FieldType::Quat: - if (!temp.quat_properties.empty()) - dest.quat_arrays.PushBack(idx, temp.quat_properties[0]); - break; - case FieldType::Transform: - if (!temp.transform_properties.empty()) - dest.transform_arrays.PushBack(idx, temp.transform_properties[0]); - break; - case FieldType::FloatRange: - if (!temp.range_properties.empty()) - dest.range_arrays.PushBack(idx, temp.range_properties[0]); - break; - default: - break; - } - } - - template - void ExtractActorWithIdFunc(const ActorPtrT src, VTX::Bucket& block, const std::string& schemaType, - IdExtractorFunc idExtractor) { - PropertyContainer& entity = block.entities.emplace_back(); - Load(src, entity, schemaType); - std::string uid = idExtractor(src); - block.unique_ids.push_back(uid); - } - - template - inline void EnsureSize(Vec& v, size_t index) { - if (v.size() <= index) { - v.resize(index + 1); - } - } - - template - inline void StoreValue(Vec& vector, size_t index, const V& val) { - EnsureSize(vector, index); - - if constexpr (std::is_same_v) { - if constexpr (std::is_convertible_v) { - vector[index] = val; - } else if constexpr (std::is_arithmetic_v) { - vector[index] = std::to_string(val); - } - } else if constexpr (std::is_assignable_v) { - vector[index] = static_cast(val); - } - } + const PropertyAddressCache* cache_; + bool debug_mode_; }; -} // namespace VTX \ No newline at end of file + +} // namespace VTX diff --git a/sdk/include/vtx/common/readers/frame_reader/loader_base.h b/sdk/include/vtx/common/readers/frame_reader/loader_base.h new file mode 100644 index 0000000..6d8008f --- /dev/null +++ b/sdk/include/vtx/common/readers/frame_reader/loader_base.h @@ -0,0 +1,267 @@ +/** +* @file loader_base.h +* @author Zenos Interactive +*/ + +#pragma once +#include "vtx/common/vtx_property_cache.h" +#include "vtx/common/vtx_types.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace VTX { + + /** + * @brief ADL conversion hook for custom client types. + * + * Clients opt in to automatic conversion by defining a free function + * `to_vtx_value` in the SAME namespace as their type, returning a + * VTX-native or std type that LoadField already understands: + * + * namespace mygame { + * struct FVector { float X, Y, Z; }; + * inline VTX::Vector to_vtx_value(const FVector& v) { return {v.X, v.Y, v.Z}; } + * } + * + * Then a StructMapping that maps `&MyStruct::position` (where + * position is FVector) "just works": GenericLoaderBase::LoadField applies + * the ADL-resolved conversion automatically before storing. + * + * @note Do NOT define to_vtx_value for VTX-native types (Vector, Quat, + * Transform, FloatRange, PropertyContainer) -- that would cause + * infinite recursion in LoadField. + */ + template + concept HasVtxConvert = requires(const T& t) { to_vtx_value(t); }; + + /** + * @brief CRTP base for the "source-format -> PropertyContainer" loader family. + * + * Shares between GenericFlatBufferLoader / GenericProtobufLoader / + * GenericNativeLoader (and future siblings) the format-agnostic plumbing: + * - LoadField(dest, struct_name, field_name, value) + * - LoadBlob + * - AppendActorList / AppendSingleEntity + * - Internal helpers: StoreValue, EnsureSize, PushToFlatArray, FillFlatArray + * + * The Derived loader MUST provide: + * - const PropertyAddress* ResolveField(int32_t entity_type_id, + * const std::string& struct_name, + * const std::string& field_name) const; + * - template + * void Load(const T& src, PropertyContainer& dest, const std::string& struct_name); + * + * Everything else flows through this base via CRTP. No virtuals, no vtable. + */ + template + class GenericLoaderBase { + public: + template + void LoadField(PropertyContainer& dest, const std::string& struct_name, const std::string& field_name, + const T& value) { + // ADL hook: if T has a `to_vtx_value` overload reachable via ADL, + // apply it first and recurse with the converted (VTX-native) value. + if constexpr (HasVtxConvert) { + this->LoadField(dest, struct_name, field_name, to_vtx_value(value)); + return; + } + + const PropertyAddress* address = AsDerived().ResolveField(dest.entity_type_id, struct_name, field_name); + if (!address) { + return; + } + + const int32_t idx = address->index; + switch (address->type_id) { + case FieldType::String: + StoreValue(dest.string_properties, idx, value); + break; + case FieldType::Int8: + case FieldType::Int32: + case FieldType::Enum: + StoreValue(dest.int32_properties, idx, value); + break; + case FieldType::Int64: + StoreValue(dest.int64_properties, idx, value); + break; + case FieldType::Float: + StoreValue(dest.float_properties, idx, value); + break; + case FieldType::Double: + StoreValue(dest.double_properties, idx, value); + break; + case FieldType::Bool: + StoreValue(dest.bool_properties, idx, value); + break; + case FieldType::Vector: + StoreValue(dest.vector_properties, idx, value); + break; + case FieldType::Quat: + StoreValue(dest.quat_properties, idx, value); + break; + case FieldType::Transform: + StoreValue(dest.transform_properties, idx, value); + break; + case FieldType::FloatRange: + StoreValue(dest.range_properties, idx, value); + break; + case FieldType::Struct: + StoreValue(dest.any_struct_properties, idx, value); + break; + case FieldType::None: + default: + break; + } + } + + void LoadBlob(PropertyContainer& dest, const std::string& struct_name, const std::string& field_name, + const void* data, size_t byte_size) { + if (!data || byte_size == 0) { + return; + } + const PropertyAddress* addr = AsDerived().ResolveField(dest.entity_type_id, struct_name, field_name); + if (!addr) { + return; + } + const uint8_t* bytes = static_cast(data); + for (size_t i = 0; i < byte_size; ++i) { + dest.byte_array_properties.PushBack(addr->index, bytes[i]); + } + } + + template + void AppendActorList(Bucket& bucket, const std::string& schema_type, const SrcIterable& src, IdFunc id_func) { + if constexpr (requires { src.size(); }) { + bucket.entities.reserve(bucket.entities.size() + src.size()); + } + for (const auto& item : src) { + ExtractActor(item, bucket, schema_type, id_func); + } + } + + template + void AppendSingleEntity(Bucket& bucket, const std::string& schema_type, const Src& src, IdFunc id_func) { + ExtractActor(src, bucket, schema_type, id_func); + } + + protected: + template + void FillFlatArray(PropertyContainer& dest, FieldType type, int32_t idx, const It& src) { + using ValueType = std::ranges::range_value_t; + constexpr bool b_is_string = + std::is_convertible_v || std::is_convertible_v; + for (const auto& it : src) { + if constexpr (b_is_string) { + if (type == FieldType::String) + dest.string_arrays.PushBack(idx, std::string(it)); + } else if constexpr (std::is_arithmetic_v) { + switch (type) { + case FieldType::Int8: + case FieldType::Int32: + case FieldType::Enum: + dest.int32_arrays.PushBack(idx, static_cast(it)); + break; + case FieldType::Int64: + dest.int64_arrays.PushBack(idx, static_cast(it)); + break; + case FieldType::Float: + dest.float_arrays.PushBack(idx, static_cast(it)); + break; + case FieldType::Double: + dest.double_arrays.PushBack(idx, static_cast(it)); + break; + case FieldType::Bool: + dest.bool_arrays.PushBack(idx, static_cast(it)); + break; + case FieldType::String: + dest.string_arrays.PushBack(idx, std::to_string(it)); + break; + case FieldType::None: + default: + break; + } + } else if constexpr (std::is_same_v) { + if (type == FieldType::Vector) + dest.vector_arrays.PushBack(idx, it); + } else if constexpr (std::is_same_v) { + if (type == FieldType::Quat) + dest.quat_arrays.PushBack(idx, it); + } else if constexpr (std::is_same_v) { + if (type == FieldType::Transform) + dest.transform_arrays.PushBack(idx, it); + } else if constexpr (std::is_same_v) { + if (type == FieldType::FloatRange) + dest.range_arrays.PushBack(idx, it); + } else if constexpr (std::is_same_v) { + if (type == FieldType::Struct) + dest.any_struct_arrays.PushBack(idx, it); + } + } + } + + void PushToFlatArray(PropertyContainer& dest, FieldType type, int32_t idx, + const PropertyContainer& temp) const { + switch (type) { + case FieldType::Vector: + if (!temp.vector_properties.empty()) + dest.vector_arrays.PushBack(idx, temp.vector_properties[0]); + break; + case FieldType::Quat: + if (!temp.quat_properties.empty()) + dest.quat_arrays.PushBack(idx, temp.quat_properties[0]); + break; + case FieldType::Transform: + if (!temp.transform_properties.empty()) + dest.transform_arrays.PushBack(idx, temp.transform_properties[0]); + break; + case FieldType::FloatRange: + if (!temp.range_properties.empty()) + dest.range_arrays.PushBack(idx, temp.range_properties[0]); + break; + case FieldType::Struct: + if (temp.entity_type_id != -1) + dest.any_struct_arrays.PushBack(idx, temp); + break; + default: + break; + } + } + + template + static void EnsureSize(Vec& v, size_t index) { + if (v.size() <= index) + v.resize(index + 1); + } + + template + static void StoreValue(Vec& vec, size_t index, const V& val) { + EnsureSize(vec, index); + using VecValueT = typename Vec::value_type; + if constexpr (std::is_same_v) { + if constexpr (std::is_convertible_v) + vec[index] = val; + else if constexpr (std::is_arithmetic_v) + vec[index] = std::to_string(val); + } else if constexpr (std::is_assignable_v) { + vec[index] = static_cast(val); + } + } + + private: + Derived& AsDerived() { return static_cast(*this); } + const Derived& AsDerived() const { return static_cast(*this); } + + template + void ExtractActor(const Src& src, Bucket& bucket, const std::string& schema_type, IdFunc id_func) { + PropertyContainer& entity = bucket.entities.emplace_back(); + AsDerived().Load(src, entity, schema_type); + bucket.unique_ids.push_back(id_func(src)); + } + }; +} // namespace VTX diff --git a/sdk/include/vtx/common/readers/frame_reader/native_loader.h b/sdk/include/vtx/common/readers/frame_reader/native_loader.h new file mode 100644 index 0000000..96d9acf --- /dev/null +++ b/sdk/include/vtx/common/readers/frame_reader/native_loader.h @@ -0,0 +1,173 @@ +/** + * @file native_loader.h + * @brief Loader that walks C++ struct instances using StructMapping + * and pushes their members into a VTX::PropertyContainer. + * @author Zenos Interactive + */ + +#pragma once + +#include "vtx/common/adapters/native/struct_mapping.h" +#include "vtx/common/readers/frame_reader/loader_base.h" +#include "vtx/common/readers/frame_reader/type_traits.h" +#include "vtx/common/vtx_property_cache.h" +#include "vtx/common/vtx_types.h" +#include "vtx/common/vtx_types_helpers.h" + +#include +#include +#include +#include +#include + +namespace VTX { + + /** + * @class GenericNativeLoader + * @brief Twin of GenericFlatBufferLoader / GenericProtobufLoader for the + * "source is a plain C++ struct" case. + * + * Inherits from GenericLoaderBase via CRTP. Implements the two required hooks: + * - ResolveField: maps (entity_type_id, field_name) -> PropertyAddress*. + * - Load: walks StructMapping::GetFields() and dispatches each field + * to LoadField / LoadStruct / LoadArray as appropriate. + */ + class GenericNativeLoader : public GenericLoaderBase { + public: + explicit GenericNativeLoader(const PropertyAddressCache& cache, bool debug = false) + : cache_(&cache) + , debug_mode_(debug) {} + + /** + * @brief Resolves a (struct, field) pair to a PropertyAddress via the cache. + * @details struct_name is unused here (the cache is keyed by entity_type_id), + * kept in the signature for symmetry with the Proto loader and for debug. + */ + const PropertyAddress* ResolveField(int32_t entity_type_id, const std::string& /*struct_name*/, + const std::string& field_name) const { + auto struct_it = cache_->structs.find(entity_type_id); + if (struct_it == cache_->structs.end()) + return nullptr; + auto prop_it = struct_it->second.properties.find(field_name); + if (prop_it == struct_it->second.properties.end()) + return nullptr; + return &prop_it->second; + } + + /** + * @brief Load a single C++ instance into a PropertyContainer. + * @details Sets dest.entity_type_id (if not yet resolved), iterates + * StructMapping::GetFields(), dispatches each field, and + * computes the container hash at the end. + */ + template + void Load(const T& src, PropertyContainer& dest, const std::string& struct_name) { + static_assert(has_struct_mapping_v, + "GenericNativeLoader::Load requires a StructMapping specialization."); + + if (dest.entity_type_id == -1) { + auto it = cache_->name_to_id.find(struct_name); + if (it != cache_->name_to_id.end()) { + dest.entity_type_id = it->second; + } + } + + constexpr auto fields = StructMapping::GetFields(); + std::apply([&](auto&&... f) { (ProcessField(src, dest, struct_name, f), ...); }, fields); + dest.content_hash = Helpers::CalculateContainerHash(dest); + } + + /** + * @brief Load a top-level frame struct into a VTX::Frame. + * @details Dispatches to StructFrameBinding::TransferToFrame, + * which the dev writes by hand (it owns the buckets/entities layout). + */ + template + void LoadFrame(const FrameT& src, VTX::Frame& dest, const std::string& schema_name) { + StructFrameBinding::TransferToFrame(src, dest, *this, schema_name); + } + + /** + * @brief Load a nested scalar struct member into dest.any_struct_properties[slot]. + */ + template + void LoadStruct(PropertyContainer& dest, const std::string& struct_name, const std::string& field_name, + const NestedT& src_nested) { + const auto* addr = ResolveField(dest.entity_type_id, struct_name, field_name); + if (!addr) { + return; + } + + EnsureSize(dest.any_struct_properties, addr->index); + Load(src_nested, dest.any_struct_properties[addr->index], addr->child_type_name); + } + + /** + * @brief Load a std::vector (or compatible range) member into the right flat array. + * @details Primitives / VTX-native types -> base's FillFlatArray (push direct). + * Elements with StructMapping<> -> recurse per element, then push the + * resulting PropertyContainer to any_struct_arrays (Struct case) or + * collapse to a flat math array via PushToFlatArray. + */ + template + void LoadArray(PropertyContainer& dest, const std::string& struct_name, const std::string& field_name, + const Container& src_array) { + if (src_array.empty()) { + return; + } + const auto* addr = ResolveField(dest.entity_type_id, struct_name, field_name); + if (!addr) { + return; + } + + using ElementT = std::ranges::range_value_t; + const int32_t idx = addr->index; + const FieldType type = addr->type_id; + + if constexpr (has_struct_mapping_v) { + const std::string& child_schema = addr->child_type_name; + for (const auto& item : src_array) { + if (type == FieldType::Struct) { + PropertyContainer child; + Load(item, child, child_schema); + dest.any_struct_arrays.PushBack(idx, std::move(child)); + } else { + PropertyContainer temp; + Load(item, temp, child_schema); + this->PushToFlatArray(dest, type, idx, temp); + } + } + } else { + this->FillFlatArray(dest, type, idx, src_array); + } + } + + private: + /** + * @brief Per-field dispatcher called from Load. + * @details Looks at the C++ type of the member and routes: + * - std::vector -> LoadArray + * - has StructMapping -> LoadStruct (recurse) + * - everything else -> base's LoadField + */ + template + void ProcessField(const T& src, PropertyContainer& dest, const std::string& struct_name, const Field& field) { + const auto& val = src.*(field.member_ptr); + using V = std::remove_cv_t>; + + if constexpr (is_vector_v) { + LoadArray(dest, struct_name, field.name, val); + } else if constexpr (has_struct_mapping_v) { + LoadStruct(dest, struct_name, field.name, val); + } else { + // Primitive (int, float, bool, std::string) or VTX-native type + // (Vector, Quat, Transform, FloatRange, PropertyContainer). + this->LoadField(dest, struct_name, field.name, val); + } + } + + const PropertyAddressCache* cache_; + bool debug_mode_; + }; + +} // namespace VTX diff --git a/sdk/include/vtx/common/readers/frame_reader/protobuff_loader.h b/sdk/include/vtx/common/readers/frame_reader/protobuff_loader.h index f7c0d23..85083ac 100644 --- a/sdk/include/vtx/common/readers/frame_reader/protobuff_loader.h +++ b/sdk/include/vtx/common/readers/frame_reader/protobuff_loader.h @@ -1,435 +1,116 @@ /** - * @file protobuffer_loader.h - * @brief Provides a generic loading mechanism from Protobuf messages to VTX native structures using a SchemaRegistry. + * @file protobuff_loader.h + * @brief Generic Protobuf -> VTX::PropertyContainer loader, on top of GenericLoaderBase. + * + * Inherits LoadField / LoadBlob / AppendActorList / AppendSingleEntity / + * StoreValue / EnsureSize / PushToFlatArray / FillFlatArray from the CRTP base. + * Implements the format-specific bits: ResolveField (cache lookup) and the + * reference-based Load / LoadStruct / LoadArray that walk Protobuf messages. + * + * Constructor still accepts a SchemaRegistry& for source-compat with existing + * callers, but only the PropertyAddressCache is retained internally. + * * @author Zenos Interactive */ #pragma once -#include -#include + +#include "vtx/common/readers/frame_reader/loader_base.h" +#include "vtx/common/readers/schema_reader/schema_registry.h" +#include "vtx/common/vtx_property_cache.h" #include "vtx/common/vtx_types.h" #include "vtx/common/vtx_types_helpers.h" +#include +#include +#include + namespace VTX { - // Forward declaration for the binding logic used by the loader template struct ProtoBinding; - /** - * @class GenericProtobufLoader - * @brief Orchestrates the transfer of data from Protobuf objects to VTX PropertyContainers. - * * This class uses a SchemaRegistry to map Protobuf fields to specific indices and types - * in the native VTX data structures, supporting nested structs, arrays, and primitive types. - */ - class GenericProtobufLoader { - private: - const SchemaRegistry* schema_; ///< Reference to the schema mapping for type_max_indices/field resolution. - bool debug_mode_; ///< If true, logs warnings and loading steps to stdout. - + class GenericProtobufLoader : public GenericLoaderBase { public: - /** - * @brief Constructs a new Generic Protobuf Loader. - * @param schema The registry containing field and type metadata. - * @param debug Enable or disable debug logging. - */ explicit GenericProtobufLoader(const SchemaRegistry& schema, bool debug = false) - : schema_(&schema) + : cache_(&schema.GetPropertyCache()) , debug_mode_(debug) {} - /** - * @brief Loads data from a Protobuf message into a native PropertyContainer. - * @tparam ProtoType The Protobuf message type generated by protoc. - * @param src The source Protobuf message. - * @param dest The target PropertyContainer to populate. - * @param struct_name The name of the structure in the SchemaRegistry. - */ + const PropertyAddress* ResolveField(int32_t entity_type_id, const std::string& /*struct_name*/, + const std::string& field_name) const { + auto struct_it = cache_->structs.find(entity_type_id); + if (struct_it == cache_->structs.end()) + return nullptr; + auto prop_it = struct_it->second.properties.find(field_name); + if (prop_it == struct_it->second.properties.end()) + return nullptr; + return &prop_it->second; + } + + template void Load(const ProtoType& src, PropertyContainer& dest, const std::string& struct_name) { - if (debug_mode_) { - std::cout << "[LOADER] Loading Struct: " << struct_name << "\n"; + if (dest.entity_type_id == -1) { + auto it = cache_->name_to_id.find(struct_name); + if (it != cache_->name_to_id.end()) { + dest.entity_type_id = it->second; + } } - dest.entity_type_id = schema_->GetStructTypeId(struct_name); ProtoBinding::Transfer(src, dest, *this, struct_name); dest.content_hash = Helpers::CalculateContainerHash(dest); } - /** - * @brief Loads a high-level Frame Protobuf message into a VTX::Frame. - * @tparam FrameProtoType The Protobuf type representing a full data frame. - * @param src The source Frame Protobuf message. - * @param dest The target VTX::Frame object. - * @param schemaName The registry name for the frame schema. - */ template - void LoadFrame(const FrameProtoType& src, VTX::Frame& dest, const std::string& schemaName) { - ProtoBinding::TransferToFrame(src, dest, *this, schemaName); + void LoadFrame(const FrameProtoType& src, VTX::Frame& dest, const std::string& schema_name) { + ProtoBinding::TransferToFrame(src, dest, *this, schema_name); } - /** - * @brief Appends a list of actors/entities from a repeated Protobuf field to a Data block. - * @tparam RepeatedProtoType A Protobuf repeated field or container. - * @tparam IdExtractorFunc A functor or lambda to extract a unique ID string from the ProtoType. - * @param targetBlock The VTX::Data block where entities will be added. - * @param schemaType The schema type name for the entities. - * @param src_array The source container of Protobuf messages. - * @param idExtractor Instance of the ID extractor function. - */ - template - void AppendActorList(VTX::Bucket& targetBlock, const std::string& schemaType, - const RepeatedProtoType& src_array, IdExtractorFunc idExtractor) { - if (src_array.empty()) - return; - - targetBlock.entities.reserve(targetBlock.entities.size() + src_array.size()); - - for (const auto& item : src_array) { - ExtractActorWithIdFunc(item, targetBlock, schemaType, idExtractor); - } - } - - /** - * @brief Appends a single actor/entity to a Data block. - * @tparam ProtoType The Protobuf message type. - * @tparam IdExtractorFunc Functor to extract the unique ID. - * @param targetBlock Target data block. - * @param schemaType Schema type name. - * @param src_item The source Protobuf message. - * @param idExtractor The ID extractor function. - */ - template - void AppendSingleActor(VTX::Bucket& targetBlock, const std::string& schemaType, const ProtoType& src_item, - IdExtractorFunc idExtractor) { - ExtractActorWithIdFunc(src_item, targetBlock, schemaType, idExtractor); - } - - /** - * @brief Sets a single field value in a PropertyContainer based on schema metadata. - * @tparam T The native C++ type of the value. - * @param dest Target PropertyContainer. - * @param struct_name Name of the parent structure in schema. - * @param field_name Name of the field to set. - * @param value The value to assign. - * @note Performs type conversion and bounds checking via EnsureSize. - */ - template - void LoadField(PropertyContainer& dest, const std::string& struct_name, const std::string& field_name, - const T& value) { - const auto* field_info = schema_->GetField(struct_name, field_name); - - if (!field_info) { - if (debug_mode_) - std::cout << " [WARNING] Field not found: " << struct_name << "::" << field_name << "\n"; - return; - } - - int32_t idx = field_info->index; - VTX::FieldType target_type = field_info->type_id; - - if (debug_mode_) { - std::cout << " [SET] " << struct_name << "::" << field_name - << " (EnumID: " << static_cast(target_type) << ")" - << " -> Index: " << idx << "\n"; - } - switch (target_type) { - case FieldType::String: - StoreValue(dest.string_properties, idx, value); - break; - case FieldType::Int8: - case FieldType::Int32: - case FieldType::Enum: - StoreValue(dest.int32_properties, idx, value); - break; - case FieldType::Int64: - StoreValue(dest.int64_properties, idx, value); - break; - case FieldType::Float: - StoreValue(dest.float_properties, idx, value); - break; - case FieldType::Double: - StoreValue(dest.double_properties, idx, value); - break; - case FieldType::Bool: - StoreValue(dest.bool_properties, idx, value); - break; - case FieldType::Vector: - StoreValue(dest.vector_properties, idx, value); - break; - case FieldType::Quat: - StoreValue(dest.quat_properties, idx, value); - break; - case FieldType::Transform: - StoreValue(dest.transform_properties, idx, value); - break; - case FieldType::FloatRange: - StoreValue(dest.range_properties, idx, value); - break; - case FieldType::Struct: - StoreValue(dest.any_struct_properties, idx, value); - break; - case FieldType::None: - default: - break; - } - } - - /** - * @brief Handles the loading of a nested Protobuf message into a recursive PropertyContainer. - * @tparam NestedProtoType The type of the nested Protobuf message. - * @param dest The parent PropertyContainer. - * @param struct_name Name of the parent structure. - * @param field_name Name of the field containing the nested struct. - * @param src_nested The source nested Protobuf message. - */ template void LoadStruct(PropertyContainer& dest, const std::string& struct_name, const std::string& field_name, const NestedProtoType& src_nested) { - const auto* field_info = schema_->GetField(struct_name, field_name); - if (!field_info) + const auto* addr = ResolveField(dest.entity_type_id, struct_name, field_name); + if (!addr) return; - int32_t index = field_info->index; - std::string child_type_name = field_info->struct_type; - - if (debug_mode_) { - std::cout << " [STRUCT] " << struct_name << "::" << field_name << " -> Index: " << index - << " (SchemaType: " << child_type_name << ")" - << "\n"; - } - - EnsureSize(dest.any_struct_properties, index); - Load(src_nested, dest.any_struct_properties[index], child_type_name); - } - - /** - * @brief Fills a flattened SoA (Structure of Arrays) from a repeated Protobuf primitive field. - * @details Iterates through the source collection and uses the PushBack method of FlatArray - * to maintain the data and offset integrity within the PropertyContainer. - * @tparam RepeatedT A Protobuf repeated field or compatible container. - * @param dest The target PropertyContainer holding the SoA structures. - * @param type The VTX FieldType identifying the specific flat array to target. - * @param idx The index of the logical sub-array within the FlatArray. - * @param src The source collection of primitive values. - */ - template - void FillFlatArray(PropertyContainer& dest, FieldType type, int32_t idx, const RepeatedT& src) { - for (const auto& val : src) { - // Compile-time check: is the value a string-like type? - constexpr bool is_string_type = std::is_convertible_v || - std::is_same_v, std::string>; - - switch (type) { - case FieldType::Int8: - case FieldType::Int32: - case FieldType::Enum: - if constexpr (!is_string_type) { - dest.int32_arrays.PushBack(idx, static_cast(val)); - } else { - if (debug_mode_) - std::cerr << "[LOADER] Error: Trying to cast a string to Int32 array.\n"; - } - break; - - case FieldType::Int64: - if constexpr (!is_string_type) { - dest.int64_arrays.PushBack(idx, static_cast(val)); - } else { - if (debug_mode_) - std::cerr << "[LOADER] Error: Trying to cast a string to Int64 array.\n"; - } - break; - - case FieldType::Float: - if constexpr (!is_string_type) { - dest.float_arrays.PushBack(idx, static_cast(val)); - } else { - if (debug_mode_) - std::cerr << "[LOADER] Error: Trying to cast a string to Float array.\n"; - } - break; - - case FieldType::Double: - if constexpr (!is_string_type) { - dest.double_arrays.PushBack(idx, static_cast(val)); - } else { - if (debug_mode_) - std::cerr << "[LOADER] Error: Trying to cast a string to Double array.\n"; - } - break; - - case FieldType::Bool: - if constexpr (!is_string_type) { - dest.bool_arrays.PushBack(idx, static_cast(val)); - } else { - if (debug_mode_) - std::cerr << "[LOADER] Error: Trying to cast a string to Bool array.\n"; - } - break; - - case FieldType::String: - if constexpr (is_string_type) { - dest.string_arrays.PushBack(idx, std::string(val)); - } else if constexpr (std::is_arithmetic_v>) { - dest.string_arrays.PushBack(idx, std::to_string(val)); - } - break; - - default: - if (debug_mode_) { - std::cerr << "[LOADER] FillFlatArray: FieldType " << static_cast(type) - << " is not a supported primitive for flat arrays.\n"; - } - break; - } - } - } - - /** - * @brief Helper to push a complex math object that was temporarily loaded into its SoA FlatArray. - * @details This is used when an array contains complex types (Vector, Quat, etc.) that the - * loader first processes into a single PropertyContainer before flattening them into the - * main SoA structure. - * * @param dest The target PropertyContainer holding the SoA arrays. - * @param type The VTX FieldType identifying which array to target. - * @param idx The specific sub-array index within the FlatArray. - * @param temp The temporary container where the single object was initially loaded. - */ - void PushToFlatArray(PropertyContainer& dest, FieldType type, int32_t idx, - const PropertyContainer& temp) const { - switch (type) { - case FieldType::Vector: - if (!temp.vector_properties.empty()) { - dest.vector_arrays.PushBack(idx, temp.vector_properties[0]); - } - break; - - case FieldType::Quat: - if (!temp.quat_properties.empty()) { - dest.quat_arrays.PushBack(idx, temp.quat_properties[0]); - } - break; - - case FieldType::Transform: - if (!temp.transform_properties.empty()) { - dest.transform_arrays.PushBack(idx, temp.transform_properties[0]); - } - break; - - case FieldType::FloatRange: - if (!temp.range_properties.empty()) { - dest.range_arrays.PushBack(idx, temp.range_properties[0]); - } - break; - - case FieldType::Struct: - if (temp.entity_type_id != -1) { - dest.any_struct_arrays.PushBack(idx, temp); - } - break; - - default: - if (debug_mode_) { - std::cerr << "[LOADER] PushToFlatArray: Type " << static_cast(type) - << " is not a complex math type_max_indices supported by SoA flattening.\n"; - } - break; - } + this->EnsureSize(dest.any_struct_properties, addr->index); + Load(src_nested, dest.any_struct_properties[addr->index], addr->child_type_name); } - /** - * @brief Serializes a repeated Protobuf field (array) into the appropriate VTX property array. - * @tparam RepeatedProtoType A Protobuf container of elements. - * @param dest Target PropertyContainer. - * @param struct_name Parent structure name. - * @param field_name Field name. - * @param src_array The source collection of values/messages. - */ template void LoadArray(PropertyContainer& dest, const std::string& struct_name, const std::string& field_name, const RepeatedProtoType& src_array) { - const auto* field_info = schema_->GetField(struct_name, field_name); - if (!field_info || src_array.empty()) + if (src_array.empty()) + return; + const auto* addr = ResolveField(dest.entity_type_id, struct_name, field_name); + if (!addr) return; using ElementT = typename RepeatedProtoType::value_type; - const int32_t idx = field_info->index; + const int32_t idx = addr->index; + const FieldType type_id = addr->type_id; if constexpr (std::is_base_of_v) { - const std::string& child_schema = field_info->struct_type; - + const std::string& child_schema = addr->child_type_name; for (const auto& item : src_array) { - if (field_info->type_id == FieldType::Struct) { + if (type_id == FieldType::Struct) { PropertyContainer child_container; Load(item, child_container, child_schema); dest.any_struct_arrays.PushBack(idx, child_container); } else { PropertyContainer temp; Load(item, temp, child_schema); - PushToFlatArray(dest, field_info->type_id, idx, temp); + this->PushToFlatArray(dest, type_id, idx, temp); } } } else { - FillFlatArray(dest, field_info->type_id, idx, src_array); - } - } - - /** - * @brief Injects a memory block (Blob) directly into byte_array_properties - */ - void LoadBlob(PropertyContainer& dest, const std::string& struct_name, const std::string& field_name, - const void* data, size_t byte_size) { - const auto* field_info = schema_->GetField(struct_name, field_name); - if (!field_info || byte_size == 0) - return; - - const uint8_t* byte_data = static_cast(data); - - for (size_t i = 0; i < byte_size; ++i) { - dest.byte_array_properties.PushBack(field_info->index, byte_data[i]); + this->FillFlatArray(dest, type_id, idx, src_array); } } private: - /** - * @brief Internal helper to load an actor and extract its ID. - * @tparam ActorT Protobuf message type of the actor. - * @tparam IdExtractorFunc ID extraction logic. - * @param src Source actor message. - * @param block Target data block. - * @param schemaType Entity schema type. - * @param idExtractor Function instance. - */ - template - void ExtractActorWithIdFunc(const ActorT& src, VTX::Bucket& block, const std::string& schemaType, - IdExtractorFunc idExtractor) { - PropertyContainer& entity = block.entities.emplace_back(); - Load(src, entity, schemaType); - Helpers::CalculateContainerHash(entity); - std::string uid = idExtractor(src); - block.unique_ids.push_back(uid); - } - - /** - * @brief Internal utility to resize a vector to accommodate a specific index. - * @tparam Vec Vector type. - * @param v Vector reference. - * @param index Desired index. - */ - template - void EnsureSize(Vec& v, size_t index) { - if (v.size() <= index) { - v.resize(index + 1); - } - } - - template - void StoreValue(Vec& vector, size_t index, const V& val) { - EnsureSize(vector, index); - if constexpr (std::is_same_v && std::is_arithmetic_v) - vector[index] = std::to_string(val); - else if constexpr (std::is_assignable_v) - vector[index] = static_cast(val); - } + const PropertyAddressCache* cache_; + bool debug_mode_; }; + } // namespace VTX diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ad40cce..5587114 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -63,6 +63,7 @@ add_executable(vtx_tests common/test_time_utils.cpp common/test_frame_accessor.cpp common/test_vtx_game_times_extended.cpp + common/test_native_loader.cpp writer/test_writer_basic.cpp writer/test_writer_edges.cpp diff --git a/tests/common/test_native_loader.cpp b/tests/common/test_native_loader.cpp new file mode 100644 index 0000000..94aeb49 --- /dev/null +++ b/tests/common/test_native_loader.cpp @@ -0,0 +1,215 @@ +// Tests for VTX::GenericNativeLoader + VTX::StructMapping and the +// ADL `to_vtx_value` hook (declared in loader_base.h). +// +// Coverage: +// - Load() walks StructMapping::GetFields() and lands every member +// in the correct PropertyContainer slot. +// - The ADL hook converts a client's custom math type into VTX::Vector +// transparently inside LoadField, without StructMapping<> ever knowing +// about the conversion. + +#include + +#include "vtx/common/adapters/native/struct_mapping.h" +#include "vtx/common/readers/frame_reader/native_loader.h" +#include "vtx/common/readers/schema_reader/schema_registry.h" +#include "vtx/common/vtx_property_cache.h" +#include "vtx/common/vtx_types.h" + +#include "util/test_fixtures.h" + +#include + +namespace vtx_native_loader_test { + + // ----------------------------------------------------------------- + // Setup 1: a player struct that already uses VTX::Vector / VTX::Quat + // ----------------------------------------------------------------- + + struct PlainPlayer { + std::string unique_id; + std::string name; + int team = 0; + float health = 100.0f; + float armor = 50.0f; + VTX::Vector position; + VTX::Quat rotation; + VTX::Vector velocity; + bool is_alive = true; + int score = 0; + int deaths = 0; + }; + + // ----------------------------------------------------------------- + // Setup 2: a player struct that uses a CUSTOM math type which the + // client converts to VTX::Vector via the ADL hook. + // ----------------------------------------------------------------- + + struct CustomVec3 { + float x = 0.0f; + float y = 0.0f; + float z = 0.0f; + }; + + // ADL hook -- defined in the SAME namespace as the custom type so that + // unqualified `to_vtx_value(v)` inside GenericLoaderBase::LoadField + // (which lives in namespace VTX) finds it via argument-dependent lookup. + inline VTX::Vector to_vtx_value(const CustomVec3& v) { + return {static_cast(v.x), static_cast(v.y), static_cast(v.z)}; + } + + struct PlayerWithCustomVec { + std::string unique_id; + std::string name; + int team = 0; + float health = 100.0f; + float armor = 50.0f; + CustomVec3 position; // <-- custom type + VTX::Quat rotation; + CustomVec3 velocity; // <-- custom type + bool is_alive = true; + int score = 0; + int deaths = 0; + }; + +} // namespace vtx_native_loader_test + +// StructMapping specializations live in namespace VTX (qualified form). + +template <> +struct VTX::StructMapping { + static constexpr auto GetFields() { + using P = vtx_native_loader_test::PlainPlayer; + return std::make_tuple(MakeStructField("UniqueID", &P::unique_id), MakeStructField("Name", &P::name), + MakeStructField("Team", &P::team), MakeStructField("Health", &P::health), + MakeStructField("Armor", &P::armor), MakeStructField("Position", &P::position), + MakeStructField("Rotation", &P::rotation), MakeStructField("Velocity", &P::velocity), + MakeStructField("IsAlive", &P::is_alive), MakeStructField("Score", &P::score), + MakeStructField("Deaths", &P::deaths)); + } +}; + +template <> +struct VTX::StructMapping { + static constexpr auto GetFields() { + using P = vtx_native_loader_test::PlayerWithCustomVec; + return std::make_tuple(MakeStructField("UniqueID", &P::unique_id), MakeStructField("Name", &P::name), + MakeStructField("Team", &P::team), MakeStructField("Health", &P::health), + MakeStructField("Armor", &P::armor), MakeStructField("Position", &P::position), + MakeStructField("Rotation", &P::rotation), MakeStructField("Velocity", &P::velocity), + MakeStructField("IsAlive", &P::is_alive), MakeStructField("Score", &P::score), + MakeStructField("Deaths", &P::deaths)); + } +}; + +namespace { + std::string SchemaPath() { + return VtxTest::FixturePath("test_schema.json"); + } +} // namespace + +// =================================================================== +// Tests +// =================================================================== + +TEST(NativeLoader, LoadsAllSlotsFromMappedStruct) { + VTX::SchemaRegistry schema; + ASSERT_TRUE(schema.LoadFromJson(SchemaPath())); + + VTX::GenericNativeLoader loader(schema.GetPropertyCache()); + + vtx_native_loader_test::PlainPlayer p {}; + p.unique_id = "player_42"; + p.name = "Alice"; + p.team = 1; + p.health = 87.5f; + p.armor = 30.0f; + p.position = {1.0, 2.0, 3.0}; + p.rotation = {0.0f, 0.0f, 0.0f, 1.0f}; + p.velocity = {-0.5, 0.0, 0.5}; + p.is_alive = true; + p.score = 5; + p.deaths = 2; + + VTX::PropertyContainer dest; + loader.Load(p, dest, "Player"); + + EXPECT_GE(dest.entity_type_id, 0); + + ASSERT_EQ(dest.string_properties.size(), 2u); + EXPECT_EQ(dest.string_properties[0], "player_42"); + EXPECT_EQ(dest.string_properties[1], "Alice"); + + ASSERT_EQ(dest.int32_properties.size(), 3u); + EXPECT_EQ(dest.int32_properties[0], 1); // Team + EXPECT_EQ(dest.int32_properties[1], 5); // Score + EXPECT_EQ(dest.int32_properties[2], 2); // Deaths + + ASSERT_EQ(dest.float_properties.size(), 2u); + EXPECT_FLOAT_EQ(dest.float_properties[0], 87.5f); + EXPECT_FLOAT_EQ(dest.float_properties[1], 30.0f); + + ASSERT_EQ(dest.vector_properties.size(), 2u); + EXPECT_DOUBLE_EQ(dest.vector_properties[0].x, 1.0); + EXPECT_DOUBLE_EQ(dest.vector_properties[0].y, 2.0); + EXPECT_DOUBLE_EQ(dest.vector_properties[0].z, 3.0); + EXPECT_DOUBLE_EQ(dest.vector_properties[1].x, -0.5); + EXPECT_DOUBLE_EQ(dest.vector_properties[1].z, 0.5); + + ASSERT_EQ(dest.quat_properties.size(), 1u); + EXPECT_FLOAT_EQ(dest.quat_properties[0].w, 1.0f); + + ASSERT_EQ(dest.bool_properties.size(), 1u); + EXPECT_TRUE(dest.bool_properties[0]); + + EXPECT_NE(dest.content_hash, 0u); +} + +TEST(NativeLoader, ADLConversionFromCustomMathType) { + VTX::SchemaRegistry schema; + ASSERT_TRUE(schema.LoadFromJson(SchemaPath())); + + VTX::GenericNativeLoader loader(schema.GetPropertyCache()); + + vtx_native_loader_test::PlayerWithCustomVec p {}; + p.unique_id = "custom_player"; + p.team = 2; + p.health = 50.0f; + p.position = {1.5f, 2.5f, 3.5f}; // CustomVec3 -> ADL -> VTX::Vector + p.velocity = {-1.0f, 0.0f, 2.0f}; // CustomVec3 -> ADL -> VTX::Vector + + VTX::PropertyContainer dest; + loader.Load(p, dest, "Player"); + + ASSERT_EQ(dest.vector_properties.size(), 2u); + + // Position: client's CustomVec3 was converted to VTX::Vector via ADL. + EXPECT_DOUBLE_EQ(dest.vector_properties[0].x, 1.5); + EXPECT_DOUBLE_EQ(dest.vector_properties[0].y, 2.5); + EXPECT_DOUBLE_EQ(dest.vector_properties[0].z, 3.5); + + // Velocity: same path, same result. + EXPECT_DOUBLE_EQ(dest.vector_properties[1].x, -1.0); + EXPECT_DOUBLE_EQ(dest.vector_properties[1].y, 0.0); + EXPECT_DOUBLE_EQ(dest.vector_properties[1].z, 2.0); + + // Non-ADL slots remain unaffected. + EXPECT_EQ(dest.int32_properties[0], 2); + EXPECT_FLOAT_EQ(dest.float_properties[0], 50.0f); +} + +TEST(NativeLoader, HasVtxConvertConceptDetectsAdlOverload) { + // The concept must see the user's hook via ADL. + static_assert(VTX::HasVtxConvert, + "ADL must find to_vtx_value(CustomVec3) in its declaring namespace."); + + // Built-in / VTX-native types must NOT satisfy the concept (nobody defined + // to_vtx_value for them, so LoadField goes through the normal switch). + static_assert(!VTX::HasVtxConvert); + static_assert(!VTX::HasVtxConvert); + static_assert(!VTX::HasVtxConvert); + static_assert(!VTX::HasVtxConvert); + static_assert(!VTX::HasVtxConvert); + + SUCCEED(); +}