initial
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
#pragma once
|
||||
|
||||
#include <cassert>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
namespace tw::serial {
|
||||
|
||||
/**
|
||||
* A flat, pre-allocated, non-owning-after-construction byte buffer used as the
|
||||
* backing store for BinaryWriter.
|
||||
*
|
||||
* Design goals:
|
||||
* - Zero heap allocation after the initial reserve().
|
||||
* - reset() in O(1) — just rewinds the write cursor.
|
||||
* - view() gives a read-only span over written bytes, usable directly as a
|
||||
* UDP payload without any copy.
|
||||
* - Not thread-safe: one buffer per client, reused every frame.
|
||||
*/
|
||||
class BinaryBuffer {
|
||||
std::vector<std::byte> m_storage;
|
||||
std::size_t m_pos{ 0 };
|
||||
|
||||
public:
|
||||
BinaryBuffer() = default;
|
||||
|
||||
explicit BinaryBuffer(std::size_t capacity) {
|
||||
m_storage.resize(capacity);
|
||||
}
|
||||
|
||||
// Reserve at least `capacity` bytes. May only grow, never shrinks.
|
||||
void reserve(std::size_t capacity) {
|
||||
if (capacity > m_storage.size()) {
|
||||
m_storage.resize(capacity);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset write cursor to 0. Does NOT zero memory — intentional for perf.
|
||||
void reset() noexcept { m_pos = 0; }
|
||||
|
||||
std::size_t size() const noexcept { return m_pos; }
|
||||
std::size_t capacity() const noexcept { return m_storage.size(); }
|
||||
bool empty() const noexcept { return m_pos == 0; }
|
||||
|
||||
// Returns a view over bytes written so far. Valid until the next write or reset.
|
||||
std::span<const std::byte> view() const noexcept {
|
||||
return { m_storage.data(), m_pos };
|
||||
}
|
||||
|
||||
// Returns a mutable span over bytes written so far.
|
||||
std::span<std::byte> mutable_view() noexcept {
|
||||
return { m_storage.data(), m_pos };
|
||||
}
|
||||
|
||||
/**
|
||||
* Append `n` raw bytes from `src`.
|
||||
* Asserts in debug; in release callers must pre-reserve enough space.
|
||||
*/
|
||||
void append(const void* src, std::size_t n) noexcept {
|
||||
assert(m_pos + n <= m_storage.size() && "BinaryBuffer overflow — call reserve() with a larger capacity");
|
||||
std::memcpy(m_storage.data() + m_pos, src, n);
|
||||
m_pos += n;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reserve `n` bytes in-place and return a pointer to them.
|
||||
* Lets callers write directly (e.g. via placement algorithms) without a
|
||||
* temporary.
|
||||
*/
|
||||
std::byte* claim(std::size_t n) noexcept {
|
||||
assert(m_pos + n <= m_storage.size() && "BinaryBuffer overflow — call reserve() with a larger capacity");
|
||||
std::byte* ptr = m_storage.data() + m_pos;
|
||||
m_pos += n;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Patch a previously written 4-byte field at `offset`.
|
||||
* Useful for writing a length prefix before the payload is known.
|
||||
*/
|
||||
void patch_u32(std::size_t offset, uint32_t value) noexcept {
|
||||
assert(offset + sizeof(uint32_t) <= m_pos);
|
||||
std::memcpy(m_storage.data() + offset, &value, sizeof(uint32_t));
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace tw::serial
|
||||
@@ -0,0 +1,222 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* tw::serial — generic, zero-allocation binary codec
|
||||
* ====================================================
|
||||
*
|
||||
* Extensibility model (ADL / explicit specialisation)
|
||||
* ---------------------------------------------------
|
||||
* To make a type T serialisable, either:
|
||||
*
|
||||
* a) Specialise tw::serial::Codec<T>:
|
||||
*
|
||||
* template<>
|
||||
* struct tw::serial::Codec<MyType> {
|
||||
* static void encode(BinaryWriter& w, const MyType& v);
|
||||
* static MyType decode(BinaryReader& r);
|
||||
* };
|
||||
*
|
||||
* b) Or provide free functions in the same namespace as T:
|
||||
*
|
||||
* void tw_serial_encode(BinaryWriter& w, const MyType& v);
|
||||
* MyType tw_serial_decode(BinaryReader& r, std::type_identity<MyType>);
|
||||
*
|
||||
* The BinaryWriter/BinaryReader forward-declared here are defined in
|
||||
* BinaryWriter.hpp / BinaryReader.hpp. Codec.hpp itself is header-only
|
||||
* and has no external dependencies beyond the C++ standard library.
|
||||
*/
|
||||
|
||||
#include "BinaryBuffer.hpp"
|
||||
|
||||
#include <bit>
|
||||
#include <concepts>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <span>
|
||||
#include <type_traits>
|
||||
|
||||
namespace tw::serial {
|
||||
|
||||
// Forward declarations
|
||||
class BinaryWriter;
|
||||
class BinaryReader;
|
||||
|
||||
// ── Primary template — intentionally incomplete so missing specialisations
|
||||
// produce a clear compiler error rather than silent wrong behaviour.
|
||||
template<typename T>
|
||||
struct Codec;
|
||||
|
||||
// ── Concept: a type is Encodable if Codec<T>::encode exists.
|
||||
template<typename T>
|
||||
concept Encodable = requires(BinaryWriter & w, const T & v) {
|
||||
Codec<T>::encode(w, v);
|
||||
};
|
||||
|
||||
// ── Concept: a type is Decodable if Codec<T>::decode exists.
|
||||
template<typename T>
|
||||
concept Decodable = requires(BinaryReader & r) {
|
||||
{ Codec<T>::decode(r) } -> std::same_as<T>;
|
||||
};
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// BinaryWriter
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Thin write-cursor over a BinaryBuffer.
|
||||
*
|
||||
* All write operations are branch-free memcpy paths for trivially-copyable
|
||||
* types. The buffer itself holds the memory; the writer is just a cursor.
|
||||
*/
|
||||
class BinaryWriter {
|
||||
BinaryBuffer& m_buf;
|
||||
|
||||
public:
|
||||
explicit BinaryWriter(BinaryBuffer& buf) noexcept : m_buf(buf) {}
|
||||
|
||||
// ── Raw bytes ────────────────────────────────────────────────────────
|
||||
|
||||
void write_bytes(const void* src, std::size_t n) noexcept {
|
||||
m_buf.append(src, n);
|
||||
}
|
||||
|
||||
void write_bytes(std::span<const std::byte> data) noexcept {
|
||||
m_buf.append(data.data(), data.size());
|
||||
}
|
||||
|
||||
// ── Primitive scalar ─────────────────────────────────────────────────
|
||||
|
||||
template<typename T>
|
||||
requires std::is_trivially_copyable_v<T>
|
||||
void write(const T& value) noexcept {
|
||||
m_buf.append(&value, sizeof(T));
|
||||
}
|
||||
|
||||
// ── Codec-dispatched write ────────────────────────────────────────────
|
||||
|
||||
template<Encodable T>
|
||||
void encode(const T& value) {
|
||||
Codec<T>::encode(*this, value);
|
||||
}
|
||||
|
||||
// ── Cursor helpers ────────────────────────────────────────────────────
|
||||
|
||||
/** Returns the current write position (useful for length-prefix patching). */
|
||||
std::size_t pos() const noexcept { return m_buf.size(); }
|
||||
|
||||
/** Reserve a 4-byte slot at the current position, return its offset. */
|
||||
std::size_t reserve_u32() noexcept {
|
||||
std::size_t offset = m_buf.size();
|
||||
uint32_t placeholder = 0;
|
||||
m_buf.append(&placeholder, sizeof(uint32_t));
|
||||
return offset;
|
||||
}
|
||||
|
||||
/** Patch a 4-byte slot previously reserved with reserve_u32(). */
|
||||
void patch_u32(std::size_t offset, uint32_t value) noexcept {
|
||||
m_buf.patch_u32(offset, value);
|
||||
}
|
||||
|
||||
BinaryBuffer& buffer() noexcept { return m_buf; }
|
||||
const BinaryBuffer& buffer() const noexcept { return m_buf; }
|
||||
|
||||
void reset() noexcept {
|
||||
m_buf.reset();
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// BinaryReader
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Read-cursor over an immutable span of bytes.
|
||||
*
|
||||
* Designed for deserialisation on the client side; does not own memory.
|
||||
* All reads advance an internal cursor. Out-of-bounds reads assert in
|
||||
* debug and invoke undefined behaviour in release (callers must validate
|
||||
* message length before feeding it to a BinaryReader).
|
||||
*/
|
||||
class BinaryReader {
|
||||
const std::byte* m_ptr;
|
||||
std::size_t m_remaining;
|
||||
|
||||
public:
|
||||
explicit BinaryReader(std::span<const std::byte> data) noexcept
|
||||
: m_ptr(data.data()), m_remaining(data.size()) {}
|
||||
|
||||
// ── Raw bytes ────────────────────────────────────────────────────────
|
||||
|
||||
void read_bytes(void* dst, std::size_t n) noexcept {
|
||||
assert(n <= m_remaining && "BinaryReader underflow");
|
||||
std::memcpy(dst, m_ptr, n);
|
||||
m_ptr += n;
|
||||
m_remaining -= n;
|
||||
}
|
||||
|
||||
std::span<const std::byte> read_bytes(std::size_t n) noexcept {
|
||||
assert(n <= m_remaining && "BinaryReader underflow");
|
||||
auto span = std::span<const std::byte>{ m_ptr, n };
|
||||
m_ptr += n;
|
||||
m_remaining -= n;
|
||||
return span;
|
||||
}
|
||||
|
||||
// ── Primitive scalar ─────────────────────────────────────────────────
|
||||
|
||||
template<typename T>
|
||||
requires std::is_trivially_copyable_v<T>
|
||||
T read() noexcept {
|
||||
T value;
|
||||
read_bytes(&value, sizeof(T));
|
||||
return value;
|
||||
}
|
||||
|
||||
// ── Codec-dispatched read ─────────────────────────────────────────────
|
||||
|
||||
template<Decodable T>
|
||||
T decode() {
|
||||
return Codec<T>::decode(*this);
|
||||
}
|
||||
|
||||
// ── State ─────────────────────────────────────────────────────────────
|
||||
|
||||
std::size_t remaining() const noexcept { return m_remaining; }
|
||||
bool empty() const noexcept { return m_remaining == 0; }
|
||||
};
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// Built-in Codec specialisations for C++ primitives
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
// All fixed-width integer and float types that are trivially copyable get
|
||||
// a direct memcpy codec — no varint encoding, deliberately, because we are
|
||||
// optimising for throughput not wire-size (and positions are floats anyway).
|
||||
|
||||
#define TW_SERIAL_TRIVIAL_CODEC(T) \
|
||||
template<> \
|
||||
struct Codec<T> { \
|
||||
static void encode(BinaryWriter& w, const T& v) noexcept { \
|
||||
w.write(v); \
|
||||
} \
|
||||
static T decode(BinaryReader& r) noexcept { \
|
||||
return r.read<T>(); \
|
||||
} \
|
||||
}
|
||||
|
||||
TW_SERIAL_TRIVIAL_CODEC(bool);
|
||||
TW_SERIAL_TRIVIAL_CODEC(uint8_t);
|
||||
TW_SERIAL_TRIVIAL_CODEC(uint16_t);
|
||||
TW_SERIAL_TRIVIAL_CODEC(uint32_t);
|
||||
TW_SERIAL_TRIVIAL_CODEC(uint64_t);
|
||||
TW_SERIAL_TRIVIAL_CODEC(int8_t);
|
||||
TW_SERIAL_TRIVIAL_CODEC(int16_t);
|
||||
TW_SERIAL_TRIVIAL_CODEC(int32_t);
|
||||
TW_SERIAL_TRIVIAL_CODEC(int64_t);
|
||||
TW_SERIAL_TRIVIAL_CODEC(float);
|
||||
TW_SERIAL_TRIVIAL_CODEC(double);
|
||||
|
||||
#undef TW_SERIAL_TRIVIAL_CODEC
|
||||
|
||||
} // namespace tw::serial
|
||||
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* Codec specialisation for entt::entity.
|
||||
*
|
||||
* Wire format: uint32_t (the raw entity storage value, 4 bytes).
|
||||
*
|
||||
* EnTT entities are 32-bit identifiers internally (version bits + index bits).
|
||||
* We transmit the raw value and let the receiver reconstruct via
|
||||
* static_cast<entt::entity>(id).
|
||||
*/
|
||||
|
||||
#include "Codec.hpp"
|
||||
|
||||
#include <entt/entt.hpp>
|
||||
|
||||
namespace tw::serial {
|
||||
|
||||
template<>
|
||||
struct Codec<entt::entity> {
|
||||
static void encode(BinaryWriter& w, const entt::entity& e) noexcept {
|
||||
auto raw = static_cast<uint32_t>(e);
|
||||
w.write(raw);
|
||||
}
|
||||
static entt::entity decode(BinaryReader& r) noexcept {
|
||||
return static_cast<entt::entity>(r.read<uint32_t>());
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace tw::serial
|
||||
@@ -0,0 +1,77 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* Codec specialisations for GLM types.
|
||||
*
|
||||
* Include this header in any translation unit that needs to
|
||||
* encode/decode GLM vectors or matrices.
|
||||
*
|
||||
* Wire format (little-endian, matches the host layout on x86/ARM LE):
|
||||
* vec2 — 2 × float (8 bytes)
|
||||
* vec3 — 3 × float (12 bytes)
|
||||
* vec4 — 4 × float (16 bytes)
|
||||
* mat4 — 16 × float (64 bytes, column-major, matching GLM's default)
|
||||
*/
|
||||
|
||||
#include "Codec.hpp"
|
||||
|
||||
#include <glm/vec2.hpp>
|
||||
#include <glm/vec3.hpp>
|
||||
#include <glm/vec4.hpp>
|
||||
#include <glm/mat4x4.hpp>
|
||||
|
||||
namespace tw::serial {
|
||||
|
||||
template<>
|
||||
struct Codec<glm::vec2> {
|
||||
static void encode(BinaryWriter& w, const glm::vec2& v) noexcept {
|
||||
w.write(v.x);
|
||||
w.write(v.y);
|
||||
}
|
||||
static glm::vec2 decode(BinaryReader& r) noexcept {
|
||||
glm::vec2 v;
|
||||
v.x = r.read<float>();
|
||||
v.y = r.read<float>();
|
||||
return v;
|
||||
}
|
||||
};
|
||||
|
||||
template<>
|
||||
struct Codec<glm::vec3> {
|
||||
static void encode(BinaryWriter& w, const glm::vec3& v) noexcept {
|
||||
// vec3 is 3 contiguous floats in GLM's layout
|
||||
w.write_bytes(&v.x, 3 * sizeof(float));
|
||||
}
|
||||
static glm::vec3 decode(BinaryReader& r) noexcept {
|
||||
glm::vec3 v;
|
||||
r.read_bytes(&v.x, 3 * sizeof(float));
|
||||
return v;
|
||||
}
|
||||
};
|
||||
|
||||
template<>
|
||||
struct Codec<glm::vec4> {
|
||||
static void encode(BinaryWriter& w, const glm::vec4& v) noexcept {
|
||||
w.write_bytes(&v.x, 4 * sizeof(float));
|
||||
}
|
||||
static glm::vec4 decode(BinaryReader& r) noexcept {
|
||||
glm::vec4 v;
|
||||
r.read_bytes(&v.x, 4 * sizeof(float));
|
||||
return v;
|
||||
}
|
||||
};
|
||||
|
||||
template<>
|
||||
struct Codec<glm::mat4> {
|
||||
static void encode(BinaryWriter& w, const glm::mat4& m) noexcept {
|
||||
// GLM mat4 is column-major; 16 contiguous floats
|
||||
w.write_bytes(&m[0][0], 16 * sizeof(float));
|
||||
}
|
||||
static glm::mat4 decode(BinaryReader& r) noexcept {
|
||||
glm::mat4 m;
|
||||
r.read_bytes(&m[0][0], 16 * sizeof(float));
|
||||
return m;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace tw::serial
|
||||
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* tw::serial — umbrella include
|
||||
*
|
||||
* Include this single header to get the full serialisation API:
|
||||
* - BinaryBuffer (backing store)
|
||||
* - BinaryWriter (write cursor + Codec dispatch)
|
||||
* - BinaryReader (read cursor + Codec dispatch)
|
||||
* - Codec<T> (extensible type trait)
|
||||
* - Built-in Codec specialisations for all C++ primitive types
|
||||
* - GlmCodec (vec2, vec3, vec4, mat4)
|
||||
* - EnttCodec (entt::entity as uint32)
|
||||
* - WorldStateWriter / WorldStateReader (game-specific high-level API)
|
||||
*/
|
||||
|
||||
#include "BinaryBuffer.hpp"
|
||||
#include "Codec.hpp"
|
||||
#include "GlmCodec.hpp"
|
||||
#include "EnttCodec.hpp"
|
||||
#include "WorldStateWriter.hpp"
|
||||
@@ -0,0 +1,261 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* WorldStateWriter / WorldStateReader
|
||||
* =====================================
|
||||
* Project-specific serialisation for the per-frame world-state snapshot
|
||||
* sent from the server to each connected client.
|
||||
*
|
||||
* Wire format (all values little-endian):
|
||||
*
|
||||
* ┌──────────────────────────────────────────────────────────┐
|
||||
* │ Header (12 bytes) │
|
||||
* │ packet_type : uint32 (PacketType::WORLD_STATE = 3) │
|
||||
* │ frame_idx : uint32 │
|
||||
* │ entity_count : uint32 (number of position records) │
|
||||
* ├──────────────────────────────────────────────────────────┤
|
||||
* │ Spawns section │
|
||||
* │ spawn_count : uint32 │
|
||||
* │ spawn[i].id : uint32 × spawn_count │
|
||||
* ├──────────────────────────────────────────────────────────┤
|
||||
* │ Despawns section │
|
||||
* │ despawn_count : uint32 │
|
||||
* │ despawn[i].id : uint32 × despawn_count │
|
||||
* ├──────────────────────────────────────────────────────────┤
|
||||
* │ Entity positions (hot path — tightly packed) │
|
||||
* │ [ id:uint32, x:float, y:float, z:float ] × entity_count│
|
||||
* └──────────────────────────────────────────────────────────┘
|
||||
*
|
||||
* Total minimum size : 20 bytes (header + empty spawns + empty despawns)
|
||||
* Per entity : 16 bytes
|
||||
* 300 entities : 20 + 300×16 = 4820 bytes (well under MTU for segmented)
|
||||
*
|
||||
* Usage (server side, called once per client per frame):
|
||||
*
|
||||
* tw::serial::WorldStateWriter w(buffer); // buffer is a BinaryBuffer
|
||||
* w.begin(frame_idx);
|
||||
* w.write_spawns(interest.spawns());
|
||||
* w.write_despawns(interest.despawns());
|
||||
* w.begin_entities(num_entities); // writes entity_count slot
|
||||
* for each entity in interest.entities():
|
||||
* w.write_entity(entity_id, position);
|
||||
* w.end(); // patches entity_count
|
||||
* // buffer.view() is ready to send
|
||||
*
|
||||
* Usage (client side):
|
||||
*
|
||||
* tw::serial::WorldStateReader r(payload_span);
|
||||
* auto header = r.read_header(); // frame_idx + counts
|
||||
* for (auto id : r.read_spawns()) { ... }
|
||||
* for (auto id : r.read_despawns()) { ... }
|
||||
* while (r.has_entity()) {
|
||||
* auto [id, pos] = r.read_entity();
|
||||
* ...
|
||||
* }
|
||||
*/
|
||||
|
||||
#include "Codec.hpp"
|
||||
#include "GlmCodec.hpp"
|
||||
#include "EnttCodec.hpp"
|
||||
|
||||
#include <entt/entt.hpp>
|
||||
#include <glm/vec3.hpp>
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
namespace tw::serial {
|
||||
|
||||
// ── Packet type tag ───────────────────────────────────────────────────────
|
||||
// Mirrors PacketType::WORLD_STATE_PACKET (value 3) in packets/Packet.hpp.
|
||||
// Hardcoded here so the serialisation module does not depend on the network
|
||||
// module — the numerical value must stay in sync if the enum changes.
|
||||
inline constexpr uint32_t kWorldStatePacketType = 3; // WORLD_STATE_PACKET
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// WorldStateWriter
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
class WorldStateWriter {
|
||||
BinaryWriter m_w;
|
||||
|
||||
// Offsets for length-prefix patching
|
||||
std::size_t m_entity_count_offset{ 0 };
|
||||
uint32_t m_entity_count{ 0 };
|
||||
|
||||
public:
|
||||
explicit WorldStateWriter(BinaryBuffer& buf) noexcept : m_w(buf) {}
|
||||
|
||||
/**
|
||||
* Write the packet header. Call once per frame, before everything else.
|
||||
* entity_count is patched in end().
|
||||
*/
|
||||
void begin(uint32_t frame_idx) noexcept {
|
||||
m_entity_count = 0;
|
||||
|
||||
// packet_type — lets the receiver dispatch without peeking further
|
||||
m_w.encode<uint32_t>(kWorldStatePacketType);
|
||||
|
||||
// frame_idx
|
||||
m_w.encode<uint32_t>(frame_idx);
|
||||
|
||||
// entity_count placeholder — patched when end() is called
|
||||
m_entity_count_offset = m_w.reserve_u32();
|
||||
}
|
||||
|
||||
// ── Spawns ────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Write the spawn list. Pass any range of entt::entity.
|
||||
*/
|
||||
template<typename Range>
|
||||
void write_spawns(const Range& spawns) noexcept {
|
||||
auto count = static_cast<uint32_t>(std::size(spawns));
|
||||
m_w.encode<uint32_t>(count);
|
||||
for (const entt::entity e : spawns) {
|
||||
m_w.encode<entt::entity>(e);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Despawns ──────────────────────────────────────────────────────────
|
||||
|
||||
template<typename Range>
|
||||
void write_despawns(const Range& despawns) noexcept {
|
||||
auto count = static_cast<uint32_t>(std::size(despawns));
|
||||
// m_w.encode<uint32_t>(count);
|
||||
for (const entt::entity e : despawns) {
|
||||
m_w.encode<entt::entity>(e);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Entity positions (the hot path) ───────────────────────────────────
|
||||
|
||||
/**
|
||||
* Write a single entity position record.
|
||||
* id : raw uint32 of the entity handle
|
||||
* pos : world-space position (x, y, z)
|
||||
*
|
||||
* This is the innermost loop of the replicator — every byte matters.
|
||||
* The compiler will inline both calls down to two contiguous memcpys.
|
||||
*/
|
||||
void write_entity(uint32_t id, const glm::vec3& pos) noexcept {
|
||||
m_w.write(id);
|
||||
// Write x, y, z as 3 contiguous floats
|
||||
m_w.write_bytes(&pos.x, 3 * sizeof(float));
|
||||
++m_entity_count;
|
||||
}
|
||||
|
||||
// Convenience overload accepting an entt::entity handle directly
|
||||
void write_entity(entt::entity entity, const glm::vec3& pos) noexcept {
|
||||
write_entity(static_cast<uint32_t>(entity), pos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Patch the entity_count field written in begin() and finalise the
|
||||
* buffer. Must be called exactly once after all write_entity() calls.
|
||||
*/
|
||||
void end() noexcept {
|
||||
m_w.patch_u32(m_entity_count_offset, m_entity_count);
|
||||
}
|
||||
|
||||
/** Expose the underlying buffer view (e.g. to pass to send_message). */
|
||||
std::span<const std::byte> view() noexcept {
|
||||
return m_w.buffer().view();
|
||||
}
|
||||
|
||||
void reset() noexcept {
|
||||
m_w.reset();
|
||||
m_entity_count = 0;
|
||||
}
|
||||
};
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
// WorldStateReader (client-side / test use)
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
struct WorldStateHeader {
|
||||
uint32_t packet_type;
|
||||
uint32_t frame_idx;
|
||||
uint32_t entity_count;
|
||||
};
|
||||
|
||||
struct EntityRecord {
|
||||
uint32_t id;
|
||||
glm::vec3 position;
|
||||
};
|
||||
|
||||
class WorldStateReader {
|
||||
BinaryReader m_r;
|
||||
WorldStateHeader m_header{};
|
||||
|
||||
uint32_t m_spawn_count{ 0 };
|
||||
uint32_t m_spawns_read{ 0 };
|
||||
|
||||
uint32_t m_despawn_count{ 0 };
|
||||
uint32_t m_despawns_read{ 0 };
|
||||
|
||||
uint32_t m_entities_read{ 0 };
|
||||
|
||||
public:
|
||||
explicit WorldStateReader(std::span<const std::byte> data) noexcept
|
||||
: m_r(data) {}
|
||||
|
||||
/** Read the 12-byte header. Must be called first. */
|
||||
WorldStateHeader read_header() noexcept {
|
||||
// m_header.packet_type = m_r.decode<uint32_t>();
|
||||
m_header.frame_idx = m_r.decode<uint32_t>();
|
||||
m_header.entity_count = m_r.decode<uint32_t>();
|
||||
|
||||
// spawn count follows immediately
|
||||
m_spawn_count = m_r.decode<uint32_t>();
|
||||
return m_header;
|
||||
}
|
||||
|
||||
/** Read the next spawn entity id. Returns 0 when exhausted. */
|
||||
bool has_spawn() const noexcept { return m_spawns_read < m_spawn_count; }
|
||||
|
||||
uint32_t read_spawn() noexcept {
|
||||
assert(has_spawn());
|
||||
++m_spawns_read;
|
||||
uint32_t id = m_r.decode<uint32_t>();
|
||||
// if (!has_spawn()) {
|
||||
// // transition to despawns
|
||||
// m_despawn_count = m_r.decode<uint32_t>();
|
||||
// m_phase = Phase::Despawns;
|
||||
// }
|
||||
return id;
|
||||
}
|
||||
|
||||
/** Skip remaining spawns and enter despawn phase. */
|
||||
void skip_spawns() noexcept {
|
||||
while (has_spawn()) read_spawn();
|
||||
}
|
||||
|
||||
bool has_despawn() const noexcept { return m_despawns_read < m_despawn_count; }
|
||||
|
||||
uint32_t read_despawn() noexcept {
|
||||
assert(has_despawn());
|
||||
++m_despawns_read;
|
||||
uint32_t id = m_r.decode<uint32_t>();
|
||||
return id;
|
||||
}
|
||||
|
||||
void skip_despawns() noexcept {
|
||||
while (has_despawn()) read_despawn();
|
||||
}
|
||||
|
||||
bool has_entity() const noexcept {
|
||||
return m_entities_read < m_header.entity_count;
|
||||
}
|
||||
|
||||
EntityRecord read_entity() noexcept {
|
||||
assert(has_entity());
|
||||
EntityRecord rec;
|
||||
rec.id = m_r.read<uint32_t>();
|
||||
m_r.read_bytes(&rec.position.x, 3 * sizeof(float));
|
||||
++m_entities_read;
|
||||
return rec;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace tw::serial
|
||||
Reference in New Issue
Block a user