e7299b17e9
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)
263 lines
9.8 KiB
C++
263 lines
9.8 KiB
C++
#include "state_serializer.h"
|
|
|
|
#include <cassert>
|
|
|
|
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<uint16_t, EntitySnapshot> &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<uint32_t>(mask));
|
|
write_entity_delta(stream, *entities[i], it->second, mask);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- Read Full ---------------------------------------------------------------
|
|
|
|
std::vector<EntitySnapshot> StateSerializer::read_full_snapshot(
|
|
Bitstream &stream,
|
|
uint32_t *out_tick
|
|
) {
|
|
std::vector<EntitySnapshot> 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<uint16_t, EntitySnapshot> &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<EntitySnapshot::ChangeMask>(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
|