Files
tactical-shooter/gdextension.disabled/simulation/src/state_serializer.cpp
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

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