diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e6f4fb7..7b89734 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -50,7 +50,7 @@ jobs: # # clang-format -i $(find sdk tools samples tests \ # -type f \( -name "*.cpp" -o -name "*.cc" -o -name "*.h" -o -name "*.hpp" \) \ - # -not -path "*/generated/*" -not -path "*arena_generated.h" \ + # -not -path "*/generated/*" -not -name "*_generated.h" \ # -not -path "*portable-file-dialogs.h") # # and commit with --git-blame-ignore-revs to preserve blame. @@ -74,11 +74,11 @@ jobs: - name: clang-format (changed lines only) run: | - # Diff-only gate: we run clang-format-diff against the unified - # diff between BASE and HEAD so only lines actually modified get - # checked. Checking the whole file would reject legacy files - # (pre .clang-format) that a PR only touched a handful of lines - # of -- that's exactly the sweep we're trying to avoid. + # Diff-only gate: only LINES touched in this PR / push get checked, + # so legacy files (pre .clang-format) aren't dragged into a sweep. + # The actual filtering, exclude rules and clang-format-diff invocation + # live in scripts/check_clang_format.py -- single source of truth + # shared with the local pre-push hook (scripts/git-hooks/pre-push). # # PR: diff against the merge base of the PR. Push: diff against # the previous commit on the same branch. @@ -89,39 +89,7 @@ jobs: BASE="HEAD~1" fi - # Scope to .cpp/.cc/.h/.hpp added or modified, excluding vendored - # and generated code. - FILES=$(git diff --name-only --diff-filter=ACM "$BASE" HEAD \ - | grep -E '\.(cpp|cc|h|hpp)$' \ - | grep -v -E '^(thirdparty/|.*generated/|.*arena_generated\.h$|.*portable-file-dialogs\.h$)' \ - || true) - - if [ -z "$FILES" ]; then - echo "No in-scope C++ files changed." - exit 0 - fi - - echo "Files in scope:" - echo "$FILES" - echo "" - - # clang-format-diff-15 ships with the clang-format-15 apt package. - # -p1 strips the a/ b/ prefix that git diff emits. - # -style=file tells it to discover .clang-format in the repo root. - PATCH=$(git diff -U0 "$BASE" HEAD -- $FILES | clang-format-diff-15 -p1 -style=file) - - if [ -n "$PATCH" ]; then - echo "::error::Formatting violations on changed lines:" - echo "$PATCH" - echo "" - echo "To fix locally for a given file:" - echo " git diff -U0 $BASE HEAD -- | clang-format-diff-15 -p1 -i" - echo "Or format the whole file (may touch unrelated legacy lines):" - echo " clang-format -i " - exit 1 - fi - - echo "All changed lines conform to .clang-format." + python scripts/check_clang_format.py --base "$BASE" build-test: name: ${{ matrix.label }} diff --git a/scripts/check_clang_format.py b/scripts/check_clang_format.py index 9b6dbe4..af00d42 100644 --- a/scripts/check_clang_format.py +++ b/scripts/check_clang_format.py @@ -25,7 +25,7 @@ EXCLUDE_PATTERNS = [ re.compile(r"^thirdparty/"), re.compile(r".*generated/"), - re.compile(r".*arena_generated\.h$"), + re.compile(r".*_generated\.h$"), # vtx_codegen.py / flatc outputs (arena_generated.h, test_schema_generated.h, ...) re.compile(r".*portable-file-dialogs\.h$"), ] diff --git a/sdk/include/vtx/common/adapters/binary/binary_cursor.h b/sdk/include/vtx/common/adapters/binary/binary_cursor.h new file mode 100644 index 0000000..bc79db4 --- /dev/null +++ b/sdk/include/vtx/common/adapters/binary/binary_cursor.h @@ -0,0 +1,285 @@ +/** + * @file binary_cursor.h + * @brief Lightweight cursor over a contiguous byte buffer for typed sequential reads. + * + * @details Owns nothing -- just a std::span view, current position + * and the source endianness. The cursor advances automatically on Read*() + * and Skip(). Bounds-checked: overrun throws std::out_of_range. + * + * Endianness defaults to native; pass std::endian::big or std::endian::little + * to byte-swap on multi-byte arithmetic reads (cross-platform binary formats). + * + * Intended use: a client writes a BinaryBinding::Transfer that walks the + * cursor with Read() / ReadString / ReadCStr / ReadLenString / SubCursor in + * the exact order the data appears in the buffer. The loader (binary_loader.h) + * then plugs each value into the right PropertyContainer slot via LoadField. + * + * @author Zenos Interactive + */ + +#pragma once + +#include +#include +#include +#include +#include // _byteswap_* on MSVC +#include +#include +#include +#include +#include + +namespace VTX { + + /** + * @class BinaryCursor + * @brief Typed forward-only reader over a byte buffer. + * + * @details Holds a non-owning std::span view plus a current + * position and a source endianness. All Read*() and Skip*() operations + * advance the position; bounds violations throw std::out_of_range. + */ + class BinaryCursor { + public: + /** + * @brief Primary constructor over a std::span. + * @details Construct via std::as_bytes(std::span{buffer}) from + * std::vector, std::array, or any contiguous range of + * byte-like values. + * @param data Read-only view over the source buffer. + * @param endian Source endianness (default: native; pass big/little to + * trigger automatic byte-swapping inside Read()). + */ + explicit BinaryCursor(std::span data, std::endian endian = std::endian::native) + : data_(data) + , pos_(0) + , endian_(endian) {} + + /** + * @brief Convenience constructor for raw (pointer, size) pairs. + * @details Provided for callers that already work with + * std::vector::data() / size() or with C-style buffers. + * @param base Pointer to the first byte of the buffer. + * @param size Number of bytes available from @p base. + * @param endian Source endianness (default: native). + */ + BinaryCursor(const uint8_t* base, size_t size, std::endian endian = std::endian::native) + : data_(reinterpret_cast(base), size) + , pos_(0) + , endian_(endian) {} + + + /** @brief Current read position in bytes from the start of the buffer. */ + size_t Tell() const { return pos_; } + + /** @brief Total size of the underlying buffer in bytes. */ + size_t Size() const { return data_.size(); } + + /** @brief Bytes still readable from the current position. */ + size_t Remaining() const { return data_.size() - pos_; } + + /** @brief True once the cursor has consumed (or passed) the whole buffer. */ + bool Eof() const { return pos_ >= data_.size(); } + + /** @brief Underlying byte view (read-only). */ + std::span Data() const { return data_; } + + /** @brief Source endianness the cursor was constructed with. */ + std::endian Endian() const { return endian_; } + + /** @brief True if Read() will byte-swap to match the native endianness. */ + bool NeedsSwap() const { return endian_ != std::endian::native; } + + + /** + * @brief Move the cursor to an absolute position in the buffer. + * @param pos New position in bytes from the start of the buffer. + * @throws std::out_of_range if @p pos > Size(). + */ + void Seek(size_t pos) { + if (pos > data_.size()) { + throw std::out_of_range("BinaryCursor::Seek beyond end of buffer"); + } + pos_ = pos; + } + + /** + * @brief Skip @p bytes from the current position without reading. + * @param bytes Number of bytes to skip forward. + * @throws std::out_of_range if the skip would overrun the buffer. + */ + void Skip(size_t bytes) { + EnsureBounds(bytes); + pos_ += bytes; + } + + /** + * @brief Advance the cursor to the next multiple of @p alignment. + * @details No-op if @p alignment is 0 or the cursor is already aligned. + * @param alignment Alignment in bytes (typically 2, 4, 8, 16). + * @throws std::out_of_range if the required padding would overrun. + */ + void AlignTo(size_t alignment) { + if (alignment == 0) { + return; + } + const size_t misalign = pos_ % alignment; + if (misalign != 0) { + Skip(alignment - misalign); + } + } + + + /** + * @brief Read a trivially-copyable value of type @p T and advance the cursor. + * @details Reads sizeof(T) bytes via memcpy (safe for unaligned access), + * advances the position, and byte-swaps the result if the cursor + * endianness differs from native AND @p T is a multi-byte + * arithmetic type. + * @tparam T Trivially-copyable type to deserialize. + * @return The deserialized value, in native byte order. + * @throws std::out_of_range if fewer than sizeof(T) bytes remain. + */ + template + T Read() { + static_assert(std::is_trivially_copyable_v, "BinaryCursor::Read: T must be trivially copyable."); + EnsureBounds(sizeof(T)); + T value; + std::memcpy(&value, data_.data() + pos_, sizeof(T)); + pos_ += sizeof(T); + if constexpr (sizeof(T) > 1 && std::is_arithmetic_v) { + if (NeedsSwap()) { + value = ByteSwap(value); + } + } + return value; + } + + + /** + * @brief Read a fixed-length string (NOT null-terminated). + * @param length Number of bytes to consume into the string. + * @return The bytes interpreted as a std::string. + * @throws std::out_of_range if @p length > Remaining(). + */ + std::string ReadString(size_t length) { + EnsureBounds(length); + std::string s(reinterpret_cast(data_.data() + pos_), length); + pos_ += length; + return s; + } + + /** + * @brief Read a null-terminated C-string, up to @p max_len bytes. + * @details Advances past the string and its null terminator (if found). + * If the terminator is not found within @p max_len (or before + * end-of-buffer), the scanned range is returned and the cursor + * stops at the last scanned byte. + * @param max_len Maximum number of bytes to scan (default: unlimited). + * @return The deserialized string (without the terminator). + */ + std::string ReadCStr(size_t max_len = SIZE_MAX) { + const size_t cap = std::min(max_len, Remaining()); + size_t len = 0; + while (len < cap && static_cast(data_[pos_ + len]) != 0) { + ++len; + } + std::string s(reinterpret_cast(data_.data() + pos_), len); + pos_ += len; + if (pos_ < data_.size() && static_cast(data_[pos_]) == 0) { + ++pos_; + } + return s; + } + + /** + * @brief Read a length-prefixed string. + * @details Reads a length value of type @p LenT (endian-swapped if needed), + * then consumes that many bytes as the string body. No null terminator. + * @tparam LenT Length-prefix type (typically uint8_t / uint16_t / uint32_t). + * @return The deserialized string. + * @throws std::out_of_range if the buffer ends before the prefix or body. + */ + template + std::string ReadLenString() { + const auto len = Read(); + return ReadString(static_cast(len)); + } + + + /** + * @brief Carve a sub-cursor of @p len bytes starting at the current position. + * @details The parent advances past the slice. The sub-cursor inherits the + * parent's endianness. Useful for nested structs with a known + * size, or for frame chunks in a stream of length-prefixed frames. + * @param len Size of the sub-region in bytes. + * @return A new BinaryCursor scoped to the carved slice. + * @throws std::out_of_range if @p len > Remaining(). + */ + BinaryCursor SubCursor(size_t len) { + EnsureBounds(len); + BinaryCursor sub(data_.subspan(pos_, len), endian_); + pos_ += len; + return sub; + } + + private: + /** + * @brief Internal bounds-check helper. + * @param n Number of additional bytes about to be consumed. + * @throws std::out_of_range if pos_ + n > data_.size(). + */ + void EnsureBounds(size_t n) const { + if (pos_ + n > data_.size()) { + throw std::out_of_range("BinaryCursor: read past end of buffer"); + } + } + + /** + * @details Delegates to the compiler intrinsic, typically a single `bswap` + * instruction on x86. Falls through to a no-op for sizes other + * than 2/4/8 bytes (e.g. char, long double). + * @tparam T Trivially-copyable arithmetic type. + * @param value Value to byte-swap. + * @return The byte-swapped value (or unchanged if size is not 2/4/8). + */ + template + static T ByteSwap(T value) { + if constexpr (sizeof(T) == 2) { + uint16_t raw; + std::memcpy(&raw, &value, sizeof(T)); +#if defined(_MSC_VER) + raw = _byteswap_ushort(raw); +#else + raw = __builtin_bswap16(raw); +#endif + std::memcpy(&value, &raw, sizeof(T)); + } else if constexpr (sizeof(T) == 4) { + uint32_t raw; + std::memcpy(&raw, &value, sizeof(T)); +#if defined(_MSC_VER) + raw = _byteswap_ulong(raw); +#else + raw = __builtin_bswap32(raw); +#endif + std::memcpy(&value, &raw, sizeof(T)); + } else if constexpr (sizeof(T) == 8) { + uint64_t raw; + std::memcpy(&raw, &value, sizeof(T)); +#if defined(_MSC_VER) + raw = _byteswap_uint64(raw); +#else + raw = __builtin_bswap64(raw); +#endif + std::memcpy(&value, &raw, sizeof(T)); + } + return value; + } + + std::span data_; + size_t pos_; + std::endian endian_; + }; + +} // namespace VTX diff --git a/sdk/include/vtx/common/readers/frame_reader/binary_loader.h b/sdk/include/vtx/common/readers/frame_reader/binary_loader.h new file mode 100644 index 0000000..89976b1 --- /dev/null +++ b/sdk/include/vtx/common/readers/frame_reader/binary_loader.h @@ -0,0 +1,150 @@ +/** + * @file binary_loader.h + * @brief Generic raw-binary -> VTX::PropertyContainer loader, on top of GenericLoaderBase. + * + * @details Fifth sibling of GenericFlatBufferLoader / GenericProtobufLoader / + * GenericNativeLoader. Designed for the case "I have a const uint8_t* + size + * and I know the layout" -- the dev writes a BinaryBinding::Transfer + * that walks a BinaryCursor with typed Read() calls, and feeds each + * value into the right slot via the inherited LoadField. + * + * No declarative schema for the binary layout: the order of Read() + * calls IS the schema. Endianness, alignment, strings, sub-buffers and + * counted arrays are the binding's responsibility (BinaryCursor provides + * the tools). + * + * @note This loader is for byte-aligned binary blobs. + * + * @author Zenos Interactive + */ + +#pragma once + +#include "vtx/common/adapters/binary/binary_cursor.h" +#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 + +namespace VTX { + + /** + * @brief Format-specific binding the dev specializes to deserialize one entity + * (Transfer) or one frame (TransferToFrame) from a BinaryCursor. + * + * @details Same role as FlatBufferBinding / ProtoBinding / + * StructFrameBinding, but the source is a BinaryCursor and @p Tag + * is an empty marker struct (there is no generated type to dispatch on). + * + * Example: + * @code + * struct PlayerBin {}; + * + * template <> + * struct VTX::BinaryBinding { + * static void Transfer(VTX::BinaryCursor& cur, VTX::PropertyContainer& dest, + * VTX::GenericBinaryLoader& loader, const std::string& schema_name) { + * loader.LoadField(dest, schema_name, "UniqueID", cur.ReadLenString()); + * loader.LoadField(dest, schema_name, "Health", cur.Read()); + * VTX::Vector pos { cur.Read(), cur.Read(), cur.Read() }; + * loader.LoadField(dest, schema_name, "Position", pos); + * } + * }; + * @endcode + * + * @tparam Tag Empty marker struct identifying the binary layout. + */ + template + struct BinaryBinding { + static_assert(sizeof(Tag) == 0, "ERROR: Missing BinaryBinding specialization."); + }; + + /** + * @class GenericBinaryLoader + * @brief Drives binary -> PropertyContainer deserialization through BinaryBinding. + * + * @details Inherits LoadField / LoadBlob / AppendActorList / AppendSingleEntity / + * StoreValue / EnsureSize / PushToFlatArray / FillFlatArray from + * GenericLoaderBase via CRTP. Implements the two format-specific hooks: + * ResolveField (cache lookup) and Load (binding dispatch). + */ + class GenericBinaryLoader : public GenericLoaderBase { + public: + /** + * @brief Construct a loader bound to a property-address cache. + * @param cache Schema-derived O(1) field-address lookup table. + * @param debug Enable verbose debug logging (currently unused). + */ + explicit GenericBinaryLoader(const PropertyAddressCache& cache, bool debug = false) + : cache_(&cache) {} + + + /** + * @brief Resolve a (struct, field) pair to a PropertyAddress via the cache. + * @details Called automatically by GenericLoaderBase::LoadField / + * LoadBlob. The @p struct_name parameter is unused here (the + * cache is keyed by entity_type_id) -- kept in the signature + * for symmetry with the proto loader and for debug logging. + * @param entity_type_id Numeric struct id, set on the destination + * PropertyContainer by Load(). + * @param struct_name Schema name of the parent struct (unused here). + * @param field_name Schema name of the field to resolve. + * @return Pointer to the cached address, or nullptr if not found. + */ + 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 Deserialize one entity from @p cursor into @p dest. + * @details Sets dest.entity_type_id from @p struct_name if not yet + * resolved, dispatches to BinaryBinding::Transfer, and + * recomputes the container content hash on exit. + * @tparam Tag Empty marker struct identifying the binary layout. + * @param cursor Source cursor; advances as the binding reads. + * @param dest Target property container (cleared / populated in place). + * @param struct_name Schema name of the destination struct. + */ + template + void Load(BinaryCursor& cursor, PropertyContainer& dest, const std::string& struct_name) { + 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; + } + } + BinaryBinding::Transfer(cursor, dest, *this, struct_name); + dest.content_hash = Helpers::CalculateContainerHash(dest); + } + + /** + * @brief Dispatch a top-level frame binding. + * @details The binding owns the buckets / entities layout -- same + * convention as the FB / Proto / Native frame bindings. + * @tparam FrameTag Empty marker struct identifying the frame layout. + * @param cursor Source cursor positioned at the start of the frame. + * @param dest Target VTX::Frame (overwritten by the binding). + * @param schema_name Schema name passed through to the binding. + */ + template + void LoadFrame(BinaryCursor& cursor, VTX::Frame& dest, const std::string& schema_name) { + BinaryBinding::TransferToFrame(cursor, dest, *this, schema_name); + } + + private: + const PropertyAddressCache* cache_; + }; + +} // 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 index 96d9acf..b71244b 100644 --- a/sdk/include/vtx/common/readers/frame_reader/native_loader.h +++ b/sdk/include/vtx/common/readers/frame_reader/native_loader.h @@ -35,8 +35,8 @@ namespace VTX { class GenericNativeLoader : public GenericLoaderBase { public: explicit GenericNativeLoader(const PropertyAddressCache& cache, bool debug = false) - : cache_(&cache) - , debug_mode_(debug) {} + : cache_(&cache) {} + /** * @brief Resolves a (struct, field) pair to a PropertyAddress via the cache. @@ -167,7 +167,6 @@ namespace VTX { } const PropertyAddressCache* cache_; - bool debug_mode_; }; } // namespace VTX diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 5587114..bbe1d18 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -64,6 +64,7 @@ add_executable(vtx_tests common/test_frame_accessor.cpp common/test_vtx_game_times_extended.cpp common/test_native_loader.cpp + common/test_binary_loader.cpp writer/test_writer_basic.cpp writer/test_writer_edges.cpp diff --git a/tests/common/test_binary_loader.cpp b/tests/common/test_binary_loader.cpp new file mode 100644 index 0000000..f832e5c --- /dev/null +++ b/tests/common/test_binary_loader.cpp @@ -0,0 +1,264 @@ +// Tests for VTX::BinaryCursor + VTX::GenericBinaryLoader + VTX::BinaryBinding. +// +// Coverage: +// - BinaryCursor: typed reads, length-prefixed strings, c-strings, sub-cursors, +// bounds checking (throws on overrun), endian swap. +// - GenericBinaryLoader: dispatch into BinaryBinding::Transfer, walking +// the cursor with Read and feeding each value through LoadField into the +// correct PropertyContainer slot. + +#include + +#include "vtx/common/adapters/binary/binary_cursor.h" +#include "vtx/common/readers/frame_reader/binary_loader.h" +#include "vtx/common/readers/schema_reader/schema_registry.h" +#include "vtx/common/vtx_types.h" + +#include "util/test_fixtures.h" +#include "util/test_schema_generated.h" // VTX::TestSchema::Player::* + +#include +#include +#include +#include +#include +#include + +namespace vtx_binary_loader_test { + + // Tag for the test entity layout. Each tag is a unique empty struct, + // analogous to a FlatBuffer-generated type or a protobuf message class -- + // it's just the discriminator for BinaryBinding<>. + struct BinPlayer {}; + + // Pack a synthetic blob for one player. Layout (little-endian): + // u16 name_len + // bytes name (name_len bytes, NOT null-terminated) + // bytes unique_id (10 bytes, null-terminated c-string) + // i32 team + // float health + // double pos.x, pos.y, pos.z + inline std::vector BuildPlayerBlob() { + std::vector buf; + + auto push_bytes = [&](const void* p, size_t n) { + const uint8_t* b = static_cast(p); + buf.insert(buf.end(), b, b + n); + }; + + // u16 name_len = 5, then "Alice" + uint16_t name_len = 5; + push_bytes(&name_len, sizeof(name_len)); + push_bytes("Alice", 5); + + // unique_id: 10-byte c-string slot "player_42" + NUL. + push_bytes("player_42\0", 10); + + int32_t team = 1; + push_bytes(&team, sizeof(team)); + + float health = 75.5f; + push_bytes(&health, sizeof(health)); + + double pos_x = 1.0, pos_y = 2.0, pos_z = 3.0; + push_bytes(&pos_x, sizeof(pos_x)); + push_bytes(&pos_y, sizeof(pos_y)); + push_bytes(&pos_z, sizeof(pos_z)); + + return buf; + } + +} // namespace vtx_binary_loader_test + +// The binding spec lives in namespace VTX (qualified specialization). +template <> +struct VTX::BinaryBinding { + static void Transfer(VTX::BinaryCursor& cur, VTX::PropertyContainer& dest, VTX::GenericBinaryLoader& loader, + const std::string& schema_name) { + // The order of Read() calls MUST match the producer's layout. + // Each value is fed into LoadField with the slot name from the schema -- + // pulled from the autogenerated VTX::TestSchema constants so a rename + // of the schema is caught at compile time, not silently at runtime. + namespace P = VTX::TestSchema::Player; + + loader.LoadField(dest, schema_name, P::Name, cur.ReadLenString()); + loader.LoadField(dest, schema_name, P::UniqueID, cur.ReadCStr(10)); + loader.LoadField(dest, schema_name, P::Team, cur.Read()); + loader.LoadField(dest, schema_name, P::Health, cur.Read()); + + VTX::Vector pos {cur.Read(), cur.Read(), cur.Read()}; + loader.LoadField(dest, schema_name, P::Position, pos); + } +}; + +namespace { + std::string SchemaPath() { + return VtxTest::FixturePath("test_schema.json"); + } +} // namespace + +// =================================================================== +// BinaryCursor: low-level read primitives +// =================================================================== + +TEST(BinaryCursor, BasicPodReadsAdvanceCursor) { + // Layout: u8, u16, u32, float, double + std::vector buf; + uint8_t b = 0xAB; + uint16_t s = 0x1234; + uint32_t i = 0xDEADBEEF; + float f = 3.14f; + double d = 2.71828; + + auto append = [&](const void* p, size_t n) { + const uint8_t* bp = static_cast(p); + buf.insert(buf.end(), bp, bp + n); + }; + append(&b, 1); + append(&s, 2); + append(&i, 4); + append(&f, 4); + append(&d, 8); + + VTX::BinaryCursor cur(buf.data(), buf.size()); + EXPECT_EQ(cur.Tell(), 0u); + EXPECT_EQ(cur.Read(), 0xAB); + EXPECT_EQ(cur.Tell(), 1u); + EXPECT_EQ(cur.Read(), 0x1234); + EXPECT_EQ(cur.Read(), 0xDEADBEEFu); + EXPECT_FLOAT_EQ(cur.Read(), 3.14f); + EXPECT_DOUBLE_EQ(cur.Read(), 2.71828); + EXPECT_TRUE(cur.Eof()); +} + +TEST(BinaryCursor, BoundsCheckThrowsOnOverrun) { + std::vector tiny {0x01, 0x02}; + VTX::BinaryCursor cur(tiny.data(), tiny.size()); + + EXPECT_NO_THROW({ (void)cur.Read(); }); + // Remaining() == 1, asking for 4 bytes must throw. + EXPECT_THROW({ (void)cur.Read(); }, std::out_of_range); +} + +TEST(BinaryCursor, ReadLenStringAndCStr) { + std::vector buf; + auto push = [&](const void* p, size_t n) { + const uint8_t* b = static_cast(p); + buf.insert(buf.end(), b, b + n); + }; + + uint16_t len = 3; + push(&len, 2); + push("foo", 3); + push("bar\0baz", 7); // c-string "bar" + NUL + "baz" (not terminated) + + VTX::BinaryCursor cur(buf.data(), buf.size()); + EXPECT_EQ(cur.ReadLenString(), "foo"); + EXPECT_EQ(cur.ReadCStr(8), "bar"); + // After ReadCStr "bar" + NUL, cursor sits at "baz". + EXPECT_EQ(cur.ReadString(3), "baz"); +} + +TEST(BinaryCursor, SubCursorCarvesRegionAndAdvancesParent) { + std::vector buf {0xAA, 0x01, 0x02, 0x03, 0x04, 0xBB}; + VTX::BinaryCursor cur(buf.data(), buf.size()); + + EXPECT_EQ(cur.Read(), 0xAA); + VTX::BinaryCursor sub = cur.SubCursor(4); + EXPECT_EQ(sub.Size(), 4u); + EXPECT_EQ(sub.Read(), 0x01); + EXPECT_EQ(sub.Read(), 0x02); + // Parent has advanced past the slice -- next read is 0xBB. + EXPECT_EQ(cur.Read(), 0xBB); + EXPECT_TRUE(cur.Eof()); +} + +TEST(BinaryCursor, EndianSwapAppliesToMultiByteArithmetic) { + // Bytes 01 02 03 04 -- big-endian uint32 == 0x01020304. + std::vector buf {0x01, 0x02, 0x03, 0x04}; + + VTX::BinaryCursor be(buf.data(), buf.size(), std::endian::big); + if constexpr (std::endian::native == std::endian::little) { + // Cursor swaps from big -> native (little). + EXPECT_EQ(be.Read(), 0x01020304u); + } else { + EXPECT_EQ(be.Read(), 0x01020304u); // already big + } + + VTX::BinaryCursor le(buf.data(), buf.size(), std::endian::little); + if constexpr (std::endian::native == std::endian::little) { + EXPECT_EQ(le.Read(), 0x04030201u); // raw little-endian read + } else { + EXPECT_EQ(le.Read(), 0x04030201u); // swap big <- little + } +} + +TEST(BinaryCursor, ConstructsFromSpanOfBytes) { + // Primary constructor takes std::span. Verify it interoperates + // with the common idioms: std::as_bytes() over a typed vector / array. + std::vector typed {0xCAFEBABE, 0xDEADBEEF}; + auto bytes = std::as_bytes(std::span(typed)); + + VTX::BinaryCursor cur(bytes); + EXPECT_EQ(cur.Size(), 8u); + EXPECT_EQ(cur.Read(), 0xCAFEBABEu); + EXPECT_EQ(cur.Read(), 0xDEADBEEFu); + EXPECT_TRUE(cur.Eof()); +} + +TEST(BinaryCursor, AlignToRoundsUp) { + std::vector buf(16, 0x00); + VTX::BinaryCursor cur(buf.data(), buf.size()); + + cur.Skip(3); + cur.AlignTo(4); + EXPECT_EQ(cur.Tell(), 4u); + cur.AlignTo(4); + EXPECT_EQ(cur.Tell(), 4u); // already aligned -> no-op + cur.Skip(1); + cur.AlignTo(8); + EXPECT_EQ(cur.Tell(), 8u); +} + +// =================================================================== +// GenericBinaryLoader: end-to-end via BinaryBinding::Transfer +// =================================================================== + +TEST(BinaryLoader, ReadsTypedFieldsFromCursorIntoPropertyContainer) { + VTX::SchemaRegistry schema; + ASSERT_TRUE(schema.LoadFromJson(SchemaPath())); + + VTX::GenericBinaryLoader loader(schema.GetPropertyCache()); + + auto blob = vtx_binary_loader_test::BuildPlayerBlob(); + VTX::BinaryCursor cursor(blob.data(), blob.size()); + + VTX::PropertyContainer dest; + loader.Load(cursor, dest, VTX::TestSchema::Player::StructName); + + EXPECT_GE(dest.entity_type_id, 0); + + // Schema-driven slot indices for "Player": + // string[0]=UniqueID string[1]=Name + // int32[0]=Team + // float[0]=Health + // vector[0]=Position + 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(), 1u); + EXPECT_EQ(dest.int32_properties[0], 1); + + ASSERT_EQ(dest.float_properties.size(), 1u); + EXPECT_FLOAT_EQ(dest.float_properties[0], 75.5f); + + ASSERT_EQ(dest.vector_properties.size(), 1u); + 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_NE(dest.content_hash, 0u); + // Cursor fully consumed. + EXPECT_TRUE(cursor.Eof()); +} diff --git a/tests/util/test_schema_generated.h b/tests/util/test_schema_generated.h new file mode 100644 index 0000000..a40f437 --- /dev/null +++ b/tests/util/test_schema_generated.h @@ -0,0 +1,563 @@ +// ============================================================================= +// Autogenerated code by vtx_codegen.py! DO NOT MODIFY! +// ============================================================================= +#pragma once +#include +#include +#include +#include "vtx/common/vtx_property_cache.h" +#include "vtx/common/vtx_frame_accessor.h" +#include "vtx/writer/core/vtx_frame_mutation_view.h" + +namespace VTX::TestSchema { + + /** @brief Strongly typed IDs for all entities in the schema */ + enum class EntityType : int32_t { + Player = 0, + Projectile = 1, + MatchState = 2, + Unknown = -1 + }; + + namespace Player { + constexpr const char* StructName = "Player"; + constexpr const char* UniqueID = "UniqueID"; + constexpr const char* Name = "Name"; + constexpr const char* Team = "Team"; + constexpr const char* Health = "Health"; + constexpr const char* Armor = "Armor"; + constexpr const char* Position = "Position"; + constexpr const char* Rotation = "Rotation"; + constexpr const char* Velocity = "Velocity"; + constexpr const char* IsAlive = "IsAlive"; + constexpr const char* Score = "Score"; + constexpr const char* Deaths = "Deaths"; + } + + class PlayerView { + private: + VTX::EntityView data_view; + const VTX::FrameAccessor& accessor; + + public: + PlayerView(VTX::EntityView view, const VTX::FrameAccessor& acc) + : data_view(view), accessor(acc) {} + + PlayerView(const VTX::PropertyContainer& container, const VTX::FrameAccessor& acc) + : data_view(container), accessor(acc) {} + + inline const std::string& GetUniqueID() const { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Player, Player::UniqueID); + return data_view.Get(cached_key); + } + + inline const std::string& GetName() const { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Player, Player::Name); + return data_view.Get(cached_key); + } + + inline int32_t GetTeam() const { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Player, Player::Team); + return data_view.Get(cached_key); + } + + inline float GetHealth() const { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Player, Player::Health); + return data_view.Get(cached_key); + } + + inline float GetArmor() const { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Player, Player::Armor); + return data_view.Get(cached_key); + } + + inline const VTX::Vector& GetPosition() const { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Player, Player::Position); + return data_view.Get(cached_key); + } + + inline const VTX::Quat& GetRotation() const { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Player, Player::Rotation); + return data_view.Get(cached_key); + } + + inline const VTX::Vector& GetVelocity() const { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Player, Player::Velocity); + return data_view.Get(cached_key); + } + + inline bool GetIsAlive() const { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Player, Player::IsAlive); + return data_view.Get(cached_key); + } + + inline int32_t GetScore() const { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Player, Player::Score); + return data_view.Get(cached_key); + } + + inline int32_t GetDeaths() const { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Player, Player::Deaths); + return data_view.Get(cached_key); + } + + }; + + class PlayerMutator { + private: + VTX::EntityMutator data_mut; + const VTX::FrameAccessor& accessor; + + public: + PlayerMutator(VTX::EntityMutator m, const VTX::FrameAccessor& acc) + : data_mut(m), accessor(acc) {} + + PlayerMutator(VTX::PropertyContainer& container, const VTX::FrameAccessor& acc) + : data_mut(container), accessor(acc) {} + + inline const std::string& GetUniqueID() const { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Player, Player::UniqueID); + return data_mut.Get(cached_key); + } + + inline void SetUniqueID(const std::string& value) { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Player, Player::UniqueID); + data_mut.Set(cached_key, value); + } + + inline const std::string& GetName() const { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Player, Player::Name); + return data_mut.Get(cached_key); + } + + inline void SetName(const std::string& value) { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Player, Player::Name); + data_mut.Set(cached_key, value); + } + + inline int32_t GetTeam() const { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Player, Player::Team); + return data_mut.Get(cached_key); + } + + inline void SetTeam(int32_t value) { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Player, Player::Team); + data_mut.Set(cached_key, value); + } + + inline float GetHealth() const { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Player, Player::Health); + return data_mut.Get(cached_key); + } + + inline void SetHealth(float value) { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Player, Player::Health); + data_mut.Set(cached_key, value); + } + + inline float GetArmor() const { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Player, Player::Armor); + return data_mut.Get(cached_key); + } + + inline void SetArmor(float value) { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Player, Player::Armor); + data_mut.Set(cached_key, value); + } + + inline const VTX::Vector& GetPosition() const { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Player, Player::Position); + return data_mut.Get(cached_key); + } + + inline void SetPosition(const VTX::Vector& value) { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Player, Player::Position); + data_mut.Set(cached_key, value); + } + + inline const VTX::Quat& GetRotation() const { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Player, Player::Rotation); + return data_mut.Get(cached_key); + } + + inline void SetRotation(const VTX::Quat& value) { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Player, Player::Rotation); + data_mut.Set(cached_key, value); + } + + inline const VTX::Vector& GetVelocity() const { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Player, Player::Velocity); + return data_mut.Get(cached_key); + } + + inline void SetVelocity(const VTX::Vector& value) { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Player, Player::Velocity); + data_mut.Set(cached_key, value); + } + + inline bool GetIsAlive() const { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Player, Player::IsAlive); + return data_mut.Get(cached_key); + } + + inline void SetIsAlive(bool value) { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Player, Player::IsAlive); + data_mut.Set(cached_key, value); + } + + inline int32_t GetScore() const { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Player, Player::Score); + return data_mut.Get(cached_key); + } + + inline void SetScore(int32_t value) { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Player, Player::Score); + data_mut.Set(cached_key, value); + } + + inline int32_t GetDeaths() const { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Player, Player::Deaths); + return data_mut.Get(cached_key); + } + + inline void SetDeaths(int32_t value) { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Player, Player::Deaths); + data_mut.Set(cached_key, value); + } + + }; + + namespace Projectile { + constexpr const char* StructName = "Projectile"; + constexpr const char* UniqueID = "UniqueID"; + constexpr const char* OwnerID = "OwnerID"; + constexpr const char* Position = "Position"; + constexpr const char* Velocity = "Velocity"; + constexpr const char* Damage = "Damage"; + constexpr const char* Type = "Type"; + } + + class ProjectileView { + private: + VTX::EntityView data_view; + const VTX::FrameAccessor& accessor; + + public: + ProjectileView(VTX::EntityView view, const VTX::FrameAccessor& acc) + : data_view(view), accessor(acc) {} + + ProjectileView(const VTX::PropertyContainer& container, const VTX::FrameAccessor& acc) + : data_view(container), accessor(acc) {} + + inline const std::string& GetUniqueID() const { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Projectile, Projectile::UniqueID); + return data_view.Get(cached_key); + } + + inline const std::string& GetOwnerID() const { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Projectile, Projectile::OwnerID); + return data_view.Get(cached_key); + } + + inline const VTX::Vector& GetPosition() const { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Projectile, Projectile::Position); + return data_view.Get(cached_key); + } + + inline const VTX::Vector& GetVelocity() const { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Projectile, Projectile::Velocity); + return data_view.Get(cached_key); + } + + inline float GetDamage() const { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Projectile, Projectile::Damage); + return data_view.Get(cached_key); + } + + inline const std::string& GetType() const { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Projectile, Projectile::Type); + return data_view.Get(cached_key); + } + + }; + + class ProjectileMutator { + private: + VTX::EntityMutator data_mut; + const VTX::FrameAccessor& accessor; + + public: + ProjectileMutator(VTX::EntityMutator m, const VTX::FrameAccessor& acc) + : data_mut(m), accessor(acc) {} + + ProjectileMutator(VTX::PropertyContainer& container, const VTX::FrameAccessor& acc) + : data_mut(container), accessor(acc) {} + + inline const std::string& GetUniqueID() const { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Projectile, Projectile::UniqueID); + return data_mut.Get(cached_key); + } + + inline void SetUniqueID(const std::string& value) { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Projectile, Projectile::UniqueID); + data_mut.Set(cached_key, value); + } + + inline const std::string& GetOwnerID() const { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Projectile, Projectile::OwnerID); + return data_mut.Get(cached_key); + } + + inline void SetOwnerID(const std::string& value) { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Projectile, Projectile::OwnerID); + data_mut.Set(cached_key, value); + } + + inline const VTX::Vector& GetPosition() const { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Projectile, Projectile::Position); + return data_mut.Get(cached_key); + } + + inline void SetPosition(const VTX::Vector& value) { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Projectile, Projectile::Position); + data_mut.Set(cached_key, value); + } + + inline const VTX::Vector& GetVelocity() const { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Projectile, Projectile::Velocity); + return data_mut.Get(cached_key); + } + + inline void SetVelocity(const VTX::Vector& value) { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Projectile, Projectile::Velocity); + data_mut.Set(cached_key, value); + } + + inline float GetDamage() const { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Projectile, Projectile::Damage); + return data_mut.Get(cached_key); + } + + inline void SetDamage(float value) { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Projectile, Projectile::Damage); + data_mut.Set(cached_key, value); + } + + inline const std::string& GetType() const { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Projectile, Projectile::Type); + return data_mut.Get(cached_key); + } + + inline void SetType(const std::string& value) { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::Projectile, Projectile::Type); + data_mut.Set(cached_key, value); + } + + }; + + namespace MatchState { + constexpr const char* StructName = "MatchState"; + constexpr const char* UniqueID = "UniqueID"; + constexpr const char* ScoreTeam1 = "ScoreTeam1"; + constexpr const char* ScoreTeam2 = "ScoreTeam2"; + constexpr const char* Round = "Round"; + constexpr const char* Phase = "Phase"; + constexpr const char* TimeRemaining = "TimeRemaining"; + } + + class MatchStateView { + private: + VTX::EntityView data_view; + const VTX::FrameAccessor& accessor; + + public: + MatchStateView(VTX::EntityView view, const VTX::FrameAccessor& acc) + : data_view(view), accessor(acc) {} + + MatchStateView(const VTX::PropertyContainer& container, const VTX::FrameAccessor& acc) + : data_view(container), accessor(acc) {} + + inline const std::string& GetUniqueID() const { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::MatchState, MatchState::UniqueID); + return data_view.Get(cached_key); + } + + inline int32_t GetScoreTeam1() const { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::MatchState, MatchState::ScoreTeam1); + return data_view.Get(cached_key); + } + + inline int32_t GetScoreTeam2() const { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::MatchState, MatchState::ScoreTeam2); + return data_view.Get(cached_key); + } + + inline int32_t GetRound() const { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::MatchState, MatchState::Round); + return data_view.Get(cached_key); + } + + inline const std::string& GetPhase() const { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::MatchState, MatchState::Phase); + return data_view.Get(cached_key); + } + + inline float GetTimeRemaining() const { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::MatchState, MatchState::TimeRemaining); + return data_view.Get(cached_key); + } + + }; + + class MatchStateMutator { + private: + VTX::EntityMutator data_mut; + const VTX::FrameAccessor& accessor; + + public: + MatchStateMutator(VTX::EntityMutator m, const VTX::FrameAccessor& acc) + : data_mut(m), accessor(acc) {} + + MatchStateMutator(VTX::PropertyContainer& container, const VTX::FrameAccessor& acc) + : data_mut(container), accessor(acc) {} + + inline const std::string& GetUniqueID() const { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::MatchState, MatchState::UniqueID); + return data_mut.Get(cached_key); + } + + inline void SetUniqueID(const std::string& value) { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::MatchState, MatchState::UniqueID); + data_mut.Set(cached_key, value); + } + + inline int32_t GetScoreTeam1() const { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::MatchState, MatchState::ScoreTeam1); + return data_mut.Get(cached_key); + } + + inline void SetScoreTeam1(int32_t value) { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::MatchState, MatchState::ScoreTeam1); + data_mut.Set(cached_key, value); + } + + inline int32_t GetScoreTeam2() const { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::MatchState, MatchState::ScoreTeam2); + return data_mut.Get(cached_key); + } + + inline void SetScoreTeam2(int32_t value) { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::MatchState, MatchState::ScoreTeam2); + data_mut.Set(cached_key, value); + } + + inline int32_t GetRound() const { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::MatchState, MatchState::Round); + return data_mut.Get(cached_key); + } + + inline void SetRound(int32_t value) { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::MatchState, MatchState::Round); + data_mut.Set(cached_key, value); + } + + inline const std::string& GetPhase() const { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::MatchState, MatchState::Phase); + return data_mut.Get(cached_key); + } + + inline void SetPhase(const std::string& value) { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::MatchState, MatchState::Phase); + data_mut.Set(cached_key, value); + } + + inline float GetTimeRemaining() const { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::MatchState, MatchState::TimeRemaining); + return data_mut.Get(cached_key); + } + + inline void SetTimeRemaining(float value) { + static VTX::PropertyKey cached_key = accessor.Get(EntityType::MatchState, MatchState::TimeRemaining); + data_mut.Set(cached_key, value); + } + + }; + + // ---------------------------------------------------------------- + // Strongly-typed iteration helpers + // ---------------------------------------------------------------- + // ForEachX(bucket, accessor, fn) walks a BucketMutator and calls + // fn(XMutator&) only for entities whose entity_type_id matches the + // struct. Read-only counterparts (XView) are available via + // ForEachXView. + + template + void ForEachPlayer(VTX::BucketMutator& bucket, const VTX::FrameAccessor& accessor, Fn fn) { + constexpr int32_t kTypeId = static_cast(EntityType::Player); + for (auto entity : bucket) { + const auto* raw = entity.raw(); + if (raw && raw->entity_type_id == kTypeId) { + PlayerMutator obj(entity, accessor); + fn(obj); + } + } + } + + template + void ForEachPlayerView(const VTX::Bucket& bucket, const VTX::FrameAccessor& accessor, Fn fn) { + constexpr int32_t kTypeId = static_cast(EntityType::Player); + for (const auto& container : bucket.entities) { + if (container.entity_type_id == kTypeId) { + PlayerView obj(container, accessor); + fn(obj); + } + } + } + + template + void ForEachProjectile(VTX::BucketMutator& bucket, const VTX::FrameAccessor& accessor, Fn fn) { + constexpr int32_t kTypeId = static_cast(EntityType::Projectile); + for (auto entity : bucket) { + const auto* raw = entity.raw(); + if (raw && raw->entity_type_id == kTypeId) { + ProjectileMutator obj(entity, accessor); + fn(obj); + } + } + } + + template + void ForEachProjectileView(const VTX::Bucket& bucket, const VTX::FrameAccessor& accessor, Fn fn) { + constexpr int32_t kTypeId = static_cast(EntityType::Projectile); + for (const auto& container : bucket.entities) { + if (container.entity_type_id == kTypeId) { + ProjectileView obj(container, accessor); + fn(obj); + } + } + } + + template + void ForEachMatchState(VTX::BucketMutator& bucket, const VTX::FrameAccessor& accessor, Fn fn) { + constexpr int32_t kTypeId = static_cast(EntityType::MatchState); + for (auto entity : bucket) { + const auto* raw = entity.raw(); + if (raw && raw->entity_type_id == kTypeId) { + MatchStateMutator obj(entity, accessor); + fn(obj); + } + } + } + + template + void ForEachMatchStateView(const VTX::Bucket& bucket, const VTX::FrameAccessor& accessor, Fn fn) { + constexpr int32_t kTypeId = static_cast(EntityType::MatchState); + for (const auto& container : bucket.entities) { + if (container.entity_type_id == kTypeId) { + MatchStateView obj(container, accessor); + fn(obj); + } + } + } + +} // namespace VTX::TestSchema \ No newline at end of file