Files
Towards/modules/serialization/include/tw/serial/WorldStateWriter.hpp
T
Martin Slachta f4174eb0c7 #1 - quicr module
2026-08-01 22:43:09 +02:00

263 lines
9.9 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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 (16 bytes) │
* │ message_type : uint32 (supplied by the caller) │
* │ sequence : uint32 (always 0, no reply expected) │
* │ 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 {
// ──────────────────────────────────────────────────────────────────────────
// 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 header. Call once per frame, before everything else.
* entity_count is patched in end().
*
* message_type is supplied by the caller so that this module stays free of
* the address the message is delivered to.
*/
void begin(uint32_t frame_idx, uint32_t message_type) noexcept {
m_entity_count = 0;
// message_type — lets the receiver dispatch without peeking further
m_w.encode<uint32_t>(message_type);
// sequence — this message is never a reply to a request
m_w.encode<uint32_t>(0);
// 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