Files
tactical-shooter/gdextension.disabled/simulation/src/bitstream.h
T
shawn e7299b17e9 Phase 7: netfox + godot-jolt stack upgrade
Stack installed:
- netfox v1.35.3 (core + extras + noray + internals)
- godot-jolt v0.16.0-stable

Architecture:
- Server: ENet transport (works headless, no netfox deps)
- Client/Editor: netfox rollback (RollbackSynchronizer, TickInterpolator)

New/modified:
- docs/migration-netfox-plan.md — migration architecture
- scripts/network/network_manager.gd — netfox-aware ENet fallback
- scripts/network/player.gd — clean base player
- client/characters/player_netfox.gd — rollback player w/ WeaponManager
- client/characters/input/player_net_input.gd — BaseNetInput subclass
- client/characters/character/fps_character_controller.gd — netfox input feed
- client/weapons/ — weapon data, registry, TacticalWeaponHitscan, WeaponManager
- client/scripts/round_replicator.gd — client-side round state bridge
- server/scripts/round_manager.gd — improved state machine
- server/scripts/plugin_api/plugin_manager.gd — refined plugin system
- config: enemy_tag, ally_tag for meatball targeting

Removed: old C++ SimulationServer GDExtension (replaced by netfox rollback)
2026-07-02 17:39:22 -04:00

261 lines
7.4 KiB
C++

#ifndef TACTICAL_SHOOTER_BITSTREAM_H
#define TACTICAL_SHOOTER_BITSTREAM_H
#include <cstdint>
#include <cstring>
#include <cassert>
#include <climits>
#include <algorithm>
#include <vector>
namespace tactical_shooter {
/**
* Bit-level read/write stream for compact network serialization.
*
* All multi-byte values are written in little-endian order regardless of
* host endianness (network byte order). Booleans pack as single bits.
* Floats can be quantized to arbitrary bit depths for bandwidth savings.
*
* Buffers are dynamically resized. Pre-allocate with reserve() to avoid
* reallocation in hot paths.
*/
class Bitstream {
public:
static constexpr size_t kMaxBufferSize = 1024 * 1024; // 1MB safety limit
Bitstream() : buffer_(), bits_written_(0), bits_read_(0) {}
explicit Bitstream(std::vector<uint8_t> data)
: buffer_(std::move(data)), bits_written_(buffer_.size() * 8), bits_read_(0) {}
// ---- Write -----------------------------------------------------------
void write_bool(bool value) {
write_bits(value ? 1 : 0, 1);
}
void write_uint8(uint8_t value) {
write_bits(value, 8);
}
void write_uint16(uint16_t value) {
write_bits(value, 16);
}
void write_uint32(uint32_t value) {
write_bits(value, 32);
}
void write_int32(int32_t value) {
// Zigzag encoding for efficient negative-number packing
uint32_t zigzag = static_cast<uint32_t>((value << 1) ^ (value >> 31));
write_bits(zigzag, 32);
}
/**
* Write a float quantized to `nbits` within [min, max].
* Storage: nbits bits. Resolution: (max-min) / (2^nbits - 1).
* Pass nbits=32 for full-precision float (no quantization loss).
*/
void write_float_quantized(float value, float min, float max, uint8_t nbits) {
assert(nbits > 0 && nbits <= 32);
if (nbits >= 32) {
// Full precision: store as raw bits
uint32_t raw;
memcpy(&raw, &value, sizeof(raw));
write_bits(raw, 32);
return;
}
float clamped = std::clamp(value, min, max);
float normalized = (clamped - min) / (max - min);
uint32_t quantized = static_cast<uint32_t>(normalized * ((1u << nbits) - 1));
write_bits(quantized, nbits);
}
/**
* Write up to `nbits` bits of `value`. LSB first packing.
*/
void write_bits(uint32_t value, uint8_t nbits) {
assert(nbits > 0 && nbits <= 32);
ensure_capacity(nbits);
uint8_t *data = buffer_.data();
size_t byte_pos = bits_written_ / 8;
uint8_t bit_offset = bits_written_ % 8;
for (uint8_t i = 0; i < nbits; ++i) {
if (value & (1u << i)) {
data[byte_pos] |= (1u << bit_offset);
}
++bit_offset;
if (bit_offset >= 8) {
bit_offset = 0;
++byte_pos;
}
}
bits_written_ += nbits;
}
// ---- Read ------------------------------------------------------------
bool read_bool() {
return read_bits(1) != 0;
}
uint8_t read_uint8() {
return static_cast<uint8_t>(read_bits(8));
}
uint16_t read_uint16() {
return static_cast<uint16_t>(read_bits(16));
}
uint32_t read_uint32() {
return read_bits(32);
}
int32_t read_int32() {
uint32_t zigzag = read_bits(32);
return static_cast<int32_t>((zigzag >> 1) ^ -(static_cast<int32_t>(zigzag & 1)));
}
/**
* Read a quantized float matching write_float_quantized().
*/
float read_float_quantized(float min, float max, uint8_t nbits) {
assert(nbits > 0 && nbits <= 32);
if (nbits >= 32) {
uint32_t raw = read_bits(32);
float value;
memcpy(&value, &raw, sizeof(value));
return value;
}
uint32_t quantized = read_bits(nbits);
float normalized = static_cast<float>(quantized) / static_cast<float>((1u << nbits) - 1);
return min + normalized * (max - min);
}
/**
* Read up to `nbits` bits, returned as LSB-packed uint32.
*/
uint32_t read_bits(uint8_t nbits) {
assert(nbits > 0 && nbits <= 32);
assert((bits_read_ + nbits) <= bits_written_);
const uint8_t *data = buffer_.data();
size_t byte_pos = bits_read_ / 8;
uint8_t bit_offset = bits_read_ % 8;
uint32_t result = 0;
for (uint8_t i = 0; i < nbits; ++i) {
if (data[byte_pos] & (1u << bit_offset)) {
result |= (1u << i);
}
++bit_offset;
if (bit_offset >= 8) {
bit_offset = 0;
++byte_pos;
}
}
bits_read_ += nbits;
return result;
}
// ---- Array helpers ---------------------------------------------------
/**
* Write a dense array of booleans packed bit-by-bit.
*/
void write_bool_array(const bool *values, size_t count) {
for (size_t i = 0; i < count; ++i) {
write_bool(values[i]);
}
}
void read_bool_array(bool *values, size_t count) {
for (size_t i = 0; i < count; ++i) {
values[i] = read_bool();
}
}
/**
* Write a variable-length array of uint8 values with a uint16 count prefix.
*/
void write_uint8_array(const uint8_t *values, uint16_t count) {
write_uint16(count);
for (uint16_t i = 0; i < count; ++i) {
write_uint8(values[i]);
}
}
std::vector<uint8_t> read_uint8_array() {
uint16_t count = read_uint16();
std::vector<uint8_t> result(count);
for (uint16_t i = 0; i < count; ++i) {
result[i] = read_uint8();
}
return result;
}
// ---- State -----------------------------------------------------------
/// Total bytes consumed by written data
size_t byte_size() const {
return (bits_written_ + 7) / 8;
}
/// Number of bits written so far
size_t bits_written() const { return bits_written_; }
/// Number of bits read so far
size_t bits_read() const { return bits_read_; }
/// Remaining readable bits
size_t bits_remaining() const {
return bits_written_ - bits_read_;
}
/// Raw buffer (const access)
const uint8_t *data() const { return buffer_.data(); }
/// Clear everything, rewind
void reset() {
buffer_.clear();
bits_written_ = 0;
bits_read_ = 0;
}
/// Pre-allocate capacity in bytes
void reserve(size_t bytes) {
buffer_.reserve(bytes);
}
/// Steal the internal buffer
std::vector<uint8_t> take_buffer() {
std::vector<uint8_t> result = std::move(buffer_);
reset();
return result;
}
private:
void ensure_capacity(uint8_t extra_bits) {
size_t needed_bytes = (bits_written_ + extra_bits + 7) / 8;
if (needed_bytes > buffer_.size()) {
if (needed_bytes > kMaxBufferSize) {
// TODO: log error instead of assert in production
assert(!"Bitstream overflow — reduce snapshot size or increase kMaxBufferSize");
}
buffer_.resize(std::max(buffer_.size() * 2, needed_bytes));
}
}
std::vector<uint8_t> buffer_;
size_t bits_written_ = 0;
size_t bits_read_ = 0;
};
} // namespace tactical_shooter
#endif // TACTICAL_SHOOTER_BITSTREAM_H