#1 - ClientWorldController refactoring

This commit is contained in:
Martin Slachta
2026-08-03 23:30:18 +02:00
parent f4174eb0c7
commit a097f4d4b0
35 changed files with 899 additions and 1010 deletions
@@ -1,59 +1,5 @@
#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"
@@ -67,10 +13,6 @@
namespace tw::serial {
// ──────────────────────────────────────────────────────────────────────────
// WorldStateWriter
// ──────────────────────────────────────────────────────────────────────────
class WorldStateWriter {
BinaryWriter m_w;
@@ -81,13 +23,6 @@ class WorldStateWriter {
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;
@@ -104,11 +39,6 @@ public:
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));
@@ -118,8 +48,6 @@ public:
}
}
// ── Despawns ──────────────────────────────────────────────────────────
template<typename Range>
void write_despawns(const Range& despawns) noexcept {
auto count = static_cast<uint32_t>(std::size(despawns));
@@ -129,16 +57,6 @@ public:
}
}
// ── 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
@@ -146,20 +64,14 @@ public:
++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();
}
@@ -170,10 +82,6 @@ public:
}
};
// ──────────────────────────────────────────────────────────────────────────
// WorldStateReader (client-side / test use)
// ──────────────────────────────────────────────────────────────────────────
struct WorldStateHeader {
uint32_t packet_type;
uint32_t frame_idx;