#include "state_serializer.h" #include namespace tactical_shooter { StateSerializer::StateSerializer(const SerializationConfig &config) : config_(config) {} // ---- Write Full -------------------------------------------------------------- void StateSerializer::write_full_snapshot( Bitstream &stream, uint32_t tick, const Entity *const *entities, uint16_t count ) { // Header: tick, entity count, base_tick=0 (full) stream.write_uint32(tick); stream.write_uint16(count); stream.write_uint16(0); // base_tick=0 signals "full snapshot" for (uint16_t i = 0; i < count; ++i) { if (entities[i] && entities[i]->is_alive()) { write_entity_full(stream, *entities[i]); } } } // ---- Write Delta ------------------------------------------------------------- void StateSerializer::write_delta_snapshot( Bitstream &stream, uint32_t tick, const Entity *const *entities, uint16_t count, const std::unordered_map &base ) { // Count changed entities first uint16_t changed_count = 0; for (uint16_t i = 0; i < count; ++i) { if (!entities[i] || !entities[i]->is_alive()) continue; uint16_t id = entities[i]->get_entity_id(); auto it = base.find(id); if (it == base.end()) { ++changed_count; // new entity = full } else { EntitySnapshot::ChangeMask mask = entities[i]->compute_change_mask(it->second); if (mask != EntitySnapshot::CHANGED_NONE) { ++changed_count; } } } // Header: tick, count, delta_tick stream.write_uint32(tick); stream.write_uint16(changed_count); // We need the tick of the base snapshot — for now write 0 (delta from // client's last acked tick which the client tracks itself). stream.write_uint16(0); // Write delta entities for (uint16_t i = 0; i < count; ++i) { if (!entities[i] || !entities[i]->is_alive()) continue; uint16_t id = entities[i]->get_entity_id(); auto it = base.find(id); if (it == base.end()) { // New entity: write full state with CHANGED_ALL stream.write_uint16(id); stream.write_uint32(EntitySnapshot::CHANGED_ALL); write_entity_full(stream, *entities[i]); } else { EntitySnapshot::ChangeMask mask = entities[i]->compute_change_mask(it->second); if (mask != EntitySnapshot::CHANGED_NONE) { stream.write_uint16(id); stream.write_uint32(static_cast(mask)); write_entity_delta(stream, *entities[i], it->second, mask); } } } } // ---- Read Full --------------------------------------------------------------- std::vector StateSerializer::read_full_snapshot( Bitstream &stream, uint32_t *out_tick ) { std::vector result; *out_tick = stream.read_uint32(); uint16_t count = stream.read_uint16(); uint16_t base_tick = stream.read_uint16(); // should be 0 for full (void)base_tick; // unused in full read result.reserve(count); for (uint16_t i = 0; i < count; ++i) { uint16_t id = stream.read_uint16(); EntitySnapshot::ChangeMask mask = read_change_mask(stream); EntitySnapshot snap = read_entity(stream, mask); snap.entity_id = id; result.push_back(snap); } return result; } // ---- Read Delta -------------------------------------------------------------- void StateSerializer::read_delta_snapshot( Bitstream &stream, std::unordered_map &state ) { uint32_t tick = stream.read_uint32(); uint16_t count = stream.read_uint16(); uint16_t base_tick = stream.read_uint16(); (void)tick; (void)base_tick; for (uint16_t i = 0; i < count; ++i) { uint16_t id = stream.read_uint16(); EntitySnapshot::ChangeMask mask = read_change_mask(stream); EntitySnapshot snap = read_entity(stream, mask); snap.entity_id = id; if (mask == EntitySnapshot::CHANGED_ALL) { state[id] = snap; // replace entirely } else { // Merge delta into existing state auto it = state.find(id); if (it != state.end()) { if (mask & EntitySnapshot::CHANGED_POSITION) it->second.position = snap.position; if (mask & EntitySnapshot::CHANGED_VELOCITY) it->second.velocity = snap.velocity; if (mask & EntitySnapshot::CHANGED_ROTATION) { it->second.yaw = snap.yaw; it->second.pitch = snap.pitch; } if (mask & EntitySnapshot::CHANGED_HEALTH) it->second.health = snap.health; if (mask & EntitySnapshot::CHANGED_ARMOR) it->second.armor = snap.armor; if (mask & EntitySnapshot::CHANGED_WEAPON) it->second.weapon_id = snap.weapon_id; if (mask & EntitySnapshot::CHANGED_AMMO) it->second.ammo = snap.ammo; if (mask & EntitySnapshot::CHANGED_FLAGS) it->second.flags = snap.flags; if (mask & EntitySnapshot::CHANGED_INPUT) it->second.last_processed_input = snap.last_processed_input; } else { state[id] = snap; // missing base, fall back to full replace } } } } // ---- Private: write ---------------------------------------------------------- void StateSerializer::write_entity_full(Bitstream &stream, const Entity &entity) { EntitySnapshot snap = entity.capture_snapshot(); write_entity_delta(stream, entity, EntitySnapshot{}, EntitySnapshot::CHANGED_ALL); } void StateSerializer::write_entity_delta( Bitstream &stream, const Entity &entity, const EntitySnapshot &base, EntitySnapshot::ChangeMask mask ) { EntitySnapshot snap = entity.capture_snapshot(); // Position if (mask & EntitySnapshot::CHANGED_POSITION) { stream.write_float_quantized(snap.position.x, config_.pos_min, config_.pos_max, config_.pos_bits); stream.write_float_quantized(snap.position.y, config_.pos_min, config_.pos_max, config_.pos_bits); stream.write_float_quantized(snap.position.z, config_.pos_min, config_.pos_max, config_.pos_bits); } // Velocity if (mask & EntitySnapshot::CHANGED_VELOCITY) { stream.write_float_quantized(snap.velocity.x, config_.vel_min, config_.vel_max, config_.vel_bits); stream.write_float_quantized(snap.velocity.y, config_.vel_min, config_.vel_max, config_.vel_bits); stream.write_float_quantized(snap.velocity.z, config_.vel_min, config_.vel_max, config_.vel_bits); } // Rotation if (mask & EntitySnapshot::CHANGED_ROTATION) { stream.write_float_quantized(snap.yaw, config_.yaw_min, config_.yaw_max, config_.yaw_bits); stream.write_float_quantized(snap.pitch, config_.pitch_min, config_.pitch_max, config_.pitch_bits); } // Health/Armor if (mask & EntitySnapshot::CHANGED_HEALTH) { stream.write_float_quantized(snap.health, config_.health_min, config_.health_max, config_.health_bits); } if (mask & EntitySnapshot::CHANGED_ARMOR) { stream.write_float_quantized(snap.armor, config_.health_min, config_.health_max, config_.health_bits); } // Weapon/Ammo if (mask & EntitySnapshot::CHANGED_WEAPON) { stream.write_uint8(snap.weapon_id); } if (mask & EntitySnapshot::CHANGED_AMMO) { stream.write_uint16(snap.ammo); } // Flags if (mask & EntitySnapshot::CHANGED_FLAGS) { stream.write_uint16(snap.flags); } // Input sequence if (mask & EntitySnapshot::CHANGED_INPUT) { stream.write_uint32(snap.last_processed_input); } } // ---- Private: read ----------------------------------------------------------- EntitySnapshot::ChangeMask StateSerializer::read_change_mask(Bitstream &stream) { return static_cast(stream.read_uint32()); } EntitySnapshot StateSerializer::read_entity( Bitstream &stream, EntitySnapshot::ChangeMask mask ) { EntitySnapshot snap; if (mask & EntitySnapshot::CHANGED_POSITION) { snap.position.x = stream.read_float_quantized(config_.pos_min, config_.pos_max, config_.pos_bits); snap.position.y = stream.read_float_quantized(config_.pos_min, config_.pos_max, config_.pos_bits); snap.position.z = stream.read_float_quantized(config_.pos_min, config_.pos_max, config_.pos_bits); } if (mask & EntitySnapshot::CHANGED_VELOCITY) { snap.velocity.x = stream.read_float_quantized(config_.vel_min, config_.vel_max, config_.vel_bits); snap.velocity.y = stream.read_float_quantized(config_.vel_min, config_.vel_max, config_.vel_bits); snap.velocity.z = stream.read_float_quantized(config_.vel_min, config_.vel_max, config_.vel_bits); } if (mask & EntitySnapshot::CHANGED_ROTATION) { snap.yaw = stream.read_float_quantized(config_.yaw_min, config_.yaw_max, config_.yaw_bits); snap.pitch = stream.read_float_quantized(config_.pitch_min, config_.pitch_max, config_.pitch_bits); } if (mask & EntitySnapshot::CHANGED_HEALTH) { snap.health = stream.read_float_quantized(config_.health_min, config_.health_max, config_.health_bits); } if (mask & EntitySnapshot::CHANGED_ARMOR) { snap.armor = stream.read_float_quantized(config_.health_min, config_.health_max, config_.health_bits); } if (mask & EntitySnapshot::CHANGED_WEAPON) { snap.weapon_id = stream.read_uint8(); } if (mask & EntitySnapshot::CHANGED_AMMO) { snap.ammo = stream.read_uint16(); } if (mask & EntitySnapshot::CHANGED_FLAGS) { snap.flags = stream.read_uint16(); } if (mask & EntitySnapshot::CHANGED_INPUT) { snap.last_processed_input = stream.read_uint32(); } return snap; } } // namespace tactical_shooter