use crate::core::types::*;
use crate::core::config::ServerConfig;
use crate::core::constants::*; use crate::core::error::ServerError;
use crate::concurrent::thread_pools::ThreadPoolSystem;
use crate::concurrent::spatial_index::ImprovedSpatialIndex;
use crate::concurrent::event_queue::PriorityEventQueue;
use crate::world::partition::{WorldPartitionManager}; use crate::entities::player::{ImprovedPlayerManager};
use crate::flatbuffers_generated::game_protocol as fb;
use crate::network::signaling::{DataChannelsMap, ClientStatesMap, ChatMessagesQueue, ClientState, handle_dc_send_error};
use crate::world::map_generator::MapGenerator;
use crate::systems::respawn::{RespawnManager, WallRespawnManager};
use crate::systems::ai::bot_ai::BotAISystem;
use crate::network::signaling::ChatMessage;
use tokio::task::JoinError;
use futures::executor;
use std::cell::RefCell;
use webrtc::data_channel::data_channel_state::RTCDataChannelState;
use parking_lot::RwLockReadGuard;
use crate::core::types::{EntityId, PlayerID, CorePickupType, MatchState};
use crate::network::signaling::PickupState;
use flatbuffers::FlatBufferBuilder;
use std::sync::Arc;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use dashmap::DashMap;
use parking_lot::RwLock as ParkingLotRwLock;
use crossbeam_queue::SegQueue;
use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering, AtomicBool};
use std::collections::{VecDeque, HashSet, HashMap};
use uuid::Uuid;
use bytes::Bytes;
use rand::Rng;
use tokio::time::sleep; use once_cell::sync::OnceCell;
use rayon::prelude::*;
use tracing::{debug, error, warn, info, trace};
use tokio::{task::JoinSet, time::timeout};
#[derive(Clone, Debug, PartialEq)]
pub struct ServerFlagState {
pub team_id: u8, pub status: fb::FlagStatus, pub position: Vec2, pub carrier_id: Option<PlayerID>, pub respawn_timer: f32, }
#[derive(Clone, Debug, PartialEq)]
pub struct ServerMatchInfo {
pub time_remaining: f32,
pub match_state: fb::MatchStateType, pub game_mode: fb::GameModeType, pub team_scores: HashMap<u8, i32>, pub flag_states: HashMap<u8, ServerFlagState>, }
impl Default for ServerMatchInfo {
fn default() -> Self {
ServerMatchInfo {
time_remaining: 300.0, match_state: fb::MatchStateType::Waiting,
game_mode: fb::GameModeType::TeamDeathmatch, team_scores: HashMap::new(),
flag_states: HashMap::new(),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum BotBehaviorState {
Idle,
MovingToPosition,
Engaging,
SeekingPickup,
Defending,
MovingToObjective,
Flanking,
Patrolling
}
struct ClientInfo {
data_channel: Arc<crate::core::types::RTCDataChannel>,
needs_initial_state: bool,
}
#[derive(Clone, Debug)]
pub struct BotController {
pub player_id: PlayerID,
pub target_position: Option<Vec2>,
pub target_enemy_id: Option<PlayerID>,
pub last_decision_time: Instant,
pub behavior_state: BotBehaviorState,
pub current_path: VecDeque<Vec2>,
pub path_recalculation_timer: Instant,
}
#[derive(Clone, Debug)]
pub struct ServerKillFeedEntry {
pub killer_name: String,
pub victim_name: String,
pub weapon: ServerWeaponType,
pub timestamp: u64,
}
#[derive(Debug)]
struct ProjectileResults {
total_processed: usize,
hits: Vec<(PlayerID, PlayerID, i32, ServerWeaponType)>, wall_hits: Vec<(EntityId, i32)>, to_remove: Vec<usize>, }
#[derive(Debug)]
struct PlayerPhysicsResults {
players_to_respawn: Vec<(PlayerID, u8)>, alive_count: usize,
}
fn map_server_weapon_to_fb(server_weapon: ServerWeaponType) -> fb::WeaponType {
match server_weapon {
ServerWeaponType::Pistol => fb::WeaponType::Pistol,
ServerWeaponType::Shotgun => fb::WeaponType::Shotgun,
ServerWeaponType::Rifle => fb::WeaponType::Rifle,
ServerWeaponType::Sniper => fb::WeaponType::Sniper,
ServerWeaponType::Melee => fb::WeaponType::Melee,
}
}
fn map_core_pickup_to_fb(core_type: &CorePickupType) -> (fb::PickupType, Option<fb::WeaponType>) {
match core_type {
CorePickupType::Health => (fb::PickupType::Health, None),
CorePickupType::Ammo => (fb::PickupType::Ammo, None),
CorePickupType::WeaponCrate(server_weapon_type) => {
(fb::PickupType::WeaponCrate, Some(map_server_weapon_to_fb(*server_weapon_type)))
}
CorePickupType::SpeedBoost => (fb::PickupType::SpeedBoost, None),
CorePickupType::DamageBoost => (fb::PickupType::DamageBoost, None),
CorePickupType::Shield => (fb::PickupType::Shield, None),
}
}
#[inline]
fn fb_safe_str<'b>(
builder: &mut flatbuffers::FlatBufferBuilder<'b>,
s: &str,
) -> flatbuffers::WIPOffset<&'b str> {
builder.create_string(s)
}
fn create_fb_player_state_for_delta<'a>(
builder: &mut flatbuffers::FlatBufferBuilder<'a>,
pstate: &PlayerState,
_changed_fields: u16, ) -> flatbuffers::WIPOffset<fb::PlayerState<'a>> {
let id_fb = fb_safe_str(builder, pstate.id.as_str());
let username_fb = fb_safe_str(builder, &pstate.username);
let weapon_fb = map_server_weapon_to_fb(pstate.weapon);
fb::PlayerState::create(
builder,
&fb::PlayerStateArgs {
id: Some(id_fb), username: Some(username_fb), x: pstate.x, y: pstate.y,
rotation: pstate.rotation, velocity_x: pstate.velocity_x, velocity_y: pstate.velocity_y,
health: pstate.health, max_health: pstate.max_health, alive: pstate.alive,
respawn_timer: pstate.respawn_timer.unwrap_or(-1.0), weapon: weapon_fb, ammo: pstate.ammo,
reload_progress: pstate.reload_progress.unwrap_or(-1.0), score: pstate.score,
kills: pstate.kills, deaths: pstate.deaths,
team_id: pstate.team_id as i8,
speed_boost_remaining: pstate.speed_boost_remaining,
damage_boost_remaining: pstate.damage_boost_remaining,
shield_current: pstate.shield_current, shield_max: pstate.shield_max,
is_carrying_flag_team_id: pstate.is_carrying_flag_team_id as i8,
},
)
}
fn build_game_event_fb<'a>(
builder: &mut flatbuffers::FlatBufferBuilder<'a>,
event: &GameEvent,
) -> flatbuffers::WIPOffset<fb::GameEvent<'a>> {
let event_pos = event_position(event);
let pos_fb = fb::Vec2::create(builder, &fb::Vec2Args { x: event_pos.x, y: event_pos.y });
let instigator_id_fb = event_instigator_id(event).map(|id| builder.create_string(id.as_str()));
let target_id_fb = event_target_id(event).map(|id| builder.create_string(&id));
let weapon_type_fb = event_weapon_type(event).map_or(fb::WeaponType::Pistol, map_server_weapon_to_fb);
fb::GameEvent::create(builder, &fb::GameEventArgs {
event_type: map_game_event_type_to_fb(event),
position: Some(pos_fb),
instigator_id: instigator_id_fb,
target_id: target_id_fb,
weapon_type: weapon_type_fb,
value: event_value(event).unwrap_or(0.0),
})
}
#[derive(Clone)]
struct SharedBroadcastData {
timestamp_ms: u64,
events: Vec<GameEvent>,
destroyed_wall_ids: Vec<EntityId>,
chat_messages: Vec<ChatMessage>,
match_info_snapshot: MatchInfoSnapshot,
kill_feed_snapshot: Vec<ServerKillFeedEntry>,
}
#[derive(Clone)]
struct MatchInfoSnapshot {
time_remaining: f32,
match_state: fb::MatchStateType,
game_mode: fb::GameModeType,
team_scores: HashMap<u8, i32>,
flag_states: HashMap<u8, ServerFlagState>,
}
pub struct MassiveGameServer {
pub config: Arc<ServerConfig>,
pub thread_pools: Arc<ThreadPoolSystem>,
pub player_manager: Arc<ImprovedPlayerManager>,
pub world_partition_manager: Arc<WorldPartitionManager>,
pub spatial_index: Arc<ImprovedSpatialIndex>,
pub projectiles_to_add: Arc<SegQueue<Projectile>>,
pub global_game_events: Arc<PriorityEventQueue>,
pub active_connections: Arc<DashMap<String, NetworkConnection>>,
pub frame_counter: Arc<AtomicU64>,
pub tick_durations_history: Arc<ParkingLotRwLock<VecDeque<Duration>>>,
pub projectiles: Arc<ParkingLotRwLock<Vec<Projectile>>>,
pub pickups: Arc<ParkingLotRwLock<Vec<Pickup>>>,
pub data_channels_map: DataChannelsMap,
pub client_states_map: ClientStatesMap,
pub chat_messages_queue: ChatMessagesQueue,
pub is_shutting_down: Arc<AtomicBool>,
pub match_info: Arc<ParkingLotRwLock<ServerMatchInfo>>,
pub kill_feed: Arc<ParkingLotRwLock<VecDeque<ServerKillFeedEntry>>>,
pub destroyed_wall_ids_this_tick: Arc<ParkingLotRwLock<HashSet<EntityId>>>,
pub updated_walls_this_tick: Arc<ParkingLotRwLock<HashMap<EntityId, Wall>>>,
pub player_aois: PlayerAoIs,
pub respawn_manager: Arc<RespawnManager>,
pub wall_respawn_manager: Arc<WallRespawnManager>,
pub bot_players: Arc<DashMap<PlayerID, BotController>>,
pub target_bot_count: Arc<AtomicU64>,
pub bot_name_counter: Arc<AtomicU64>,
pub last_broadcast_frame: Arc<AtomicU64>,
pub player_last_sync_positions: Arc<DashMap<PlayerID, (f32, f32)>>,
}
const MAX_KILL_FEED_HISTORY: usize = 10;
static CACHED_WALLS: OnceCell<Arc<ParkingLotRwLock<(u64, Vec<Wall>)>>> = OnceCell::new();
impl MassiveGameServer {
pub fn new(
config: Arc<ServerConfig>,
thread_pools: Arc<ThreadPoolSystem>,
data_channels_map: DataChannelsMap,
client_states_map: ClientStatesMap,
chat_messages_queue: ChatMessagesQueue,
player_aois: PlayerAoIs,
) -> Self {
info!("Initializing MassiveGameServer...");
let spatial_index = Arc::new(ImprovedSpatialIndex::new(
WORLD_MAX_X - WORLD_MIN_X, WORLD_MAX_Y - WORLD_MIN_Y,
WORLD_MIN_X, WORLD_MIN_Y, SPATIAL_INDEX_CELL_SIZE,
));
info!("Spatial index initialized.");
let player_manager = Arc::new(ImprovedPlayerManager::new(
config.num_player_shards,
spatial_index.clone(),
));
info!("Player manager initialized with {} shards.", config.num_player_shards);
let all_map_walls = MapGenerator::generate_10v10_map();
info!("Generated {} walls for the map.", all_map_walls.len());
let world_partition_manager = Arc::new(WorldPartitionManager::new(
config.world_partition_grid_dim,
WORLD_MAX_X - WORLD_MIN_X,
WORLD_MAX_Y - WORLD_MIN_Y,
WORLD_MIN_X,
WORLD_MIN_Y,
1024,
));
info!("World partition manager initialized with {}x{} grid.", config.world_partition_grid_dim, config.world_partition_grid_dim);
for wall in &all_map_walls {
let wall_center_x = wall.x + wall.width / 2.0;
let wall_center_y = wall.y + wall.height / 2.0;
let partition_idx = world_partition_manager.get_partition_index_for_point(wall_center_x, wall_center_y);
if let Some(partition) = world_partition_manager.get_partition(partition_idx) {
partition.add_wall_on_load(wall.clone());
} else {
error!("Could not find partition with index {} for wall {}", partition_idx, wall.id);
}
}
info!("Distributed walls to partitions.");
CACHED_WALLS.get_or_init(|| {
let mut initial_walls_vec = Vec::new();
for partition in world_partition_manager.get_partitions_for_processing() {
for entry in partition.all_walls_in_partition.iter() {
initial_walls_vec.push(entry.value().clone());
}
}
info!("[Wall Cache Initial Population] Populating wall cache in new() with {} structural walls.", initial_walls_vec.len());
Arc::new(ParkingLotRwLock::new((0, initial_walls_vec))) });
let respawn_manager = Arc::new(RespawnManager::new());
let wall_respawn_manager = Arc::new(WallRespawnManager::new());
let destructible_walls_vec: Vec<Wall> = all_map_walls.iter()
.filter(|w| w.is_destructible)
.cloned()
.collect();
wall_respawn_manager.register_all_walls(&destructible_walls_vec);
info!("Registered {} destructible walls with WallRespawnManager.", destructible_walls_vec.len());
let initial_pickups = Self::generate_initial_pickups(&all_map_walls);
info!("Generated {} initial pickups.", initial_pickups.len());
let server = MassiveGameServer {
config,
thread_pools,
player_manager,
world_partition_manager,
spatial_index,
projectiles_to_add: Arc::new(SegQueue::new()),
global_game_events: Arc::new(PriorityEventQueue::new()),
active_connections: Arc::new(DashMap::new()),
frame_counter: Arc::new(AtomicU64::new(0)),
tick_durations_history: Arc::new(ParkingLotRwLock::new(VecDeque::with_capacity(1000))),
projectiles: Arc::new(ParkingLotRwLock::new(Vec::new())),
pickups: Arc::new(ParkingLotRwLock::new(initial_pickups)),
data_channels_map,
client_states_map,
chat_messages_queue,
is_shutting_down: Arc::new(AtomicBool::new(false)),
match_info: Arc::new(ParkingLotRwLock::new(ServerMatchInfo::default())),
kill_feed: Arc::new(ParkingLotRwLock::new(VecDeque::with_capacity(MAX_KILL_FEED_HISTORY + 5))),
destroyed_wall_ids_this_tick: Arc::new(ParkingLotRwLock::new(HashSet::new())),
updated_walls_this_tick: Arc::new(ParkingLotRwLock::new(HashMap::new())),
player_aois,
respawn_manager,
wall_respawn_manager,
bot_players: Arc::new(DashMap::new()),
target_bot_count: Arc::new(AtomicU64::new(20)),
bot_name_counter: Arc::new(AtomicU64::new(0)),
last_broadcast_frame: Arc::new(AtomicU64::new(0)),
player_last_sync_positions: Arc::new(DashMap::new()),
};
let initial_bot_count = server.target_bot_count.load(AtomicOrdering::Relaxed);
server.spawn_initial_bots(initial_bot_count as usize);
info!("MassiveGameServer initialized successfully.");
server
}
fn generate_initial_pickups(map_walls: &[Wall]) -> Vec<Pickup> {
let mut pickups = Vec::new();
let mut rng = rand::thread_rng();
let pickup_types = [
CorePickupType::Health, CorePickupType::Ammo,
CorePickupType::WeaponCrate(ServerWeaponType::Shotgun),
CorePickupType::WeaponCrate(ServerWeaponType::Rifle),
CorePickupType::SpeedBoost, CorePickupType::DamageBoost, CorePickupType::Shield,
CorePickupType::WeaponCrate(ServerWeaponType::Sniper),
];
let strategic_locations = [
Vec2::new(0.0, 0.0),
Vec2::new(WORLD_MIN_X / 2.0, WORLD_MIN_Y / 2.0),
Vec2::new(WORLD_MAX_X / 2.0, WORLD_MIN_Y / 2.0),
Vec2::new(WORLD_MIN_X / 2.0, WORLD_MAX_Y / 2.0),
Vec2::new(WORLD_MAX_X / 2.0, WORLD_MAX_Y / 2.0),
Vec2::new(WORLD_MIN_X + 250.0, 0.0),
Vec2::new(WORLD_MAX_X - 250.0, 0.0),
];
let num_pickups_to_spawn = strategic_locations.len().min(pickup_types.len());
for i in 0..num_pickups_to_spawn {
let base_pos = strategic_locations[i % strategic_locations.len()];
let mut placed = false;
for _attempt in 0..10 {
let x_offset = rng.gen_range(-50.0..50.0);
let y_offset = rng.gen_range(-50.0..50.0);
let x = (base_pos.x + x_offset).clamp(WORLD_MIN_X + 50.0, WORLD_MAX_X - 50.0);
let y = (base_pos.y + y_offset).clamp(WORLD_MIN_Y + 50.0, WORLD_MAX_Y - 50.0);
let mut obstructed = false;
for wall in map_walls {
if wall.is_destructible && wall.current_health <= 0 { continue; }
if x + PICKUP_COLLECTION_RADIUS > wall.x && x - PICKUP_COLLECTION_RADIUS < wall.x + wall.width &&
y + PICKUP_COLLECTION_RADIUS > wall.y && y - PICKUP_COLLECTION_RADIUS < wall.y + wall.height {
obstructed = true; break;
}
}
if !obstructed {
let pickup_type = pickup_types[i % pickup_types.len()].clone();
pickups.push(Pickup::new(Uuid::new_v4().as_u128() as u64, x, y, pickup_type));
placed = true; break;
}
}
if !placed { warn!("Could not place pickup {} near {:?} after 10 attempts.", i, base_pos); }
}
pickups
}
fn spawn_initial_bots(&self, count: usize) {
info!("Spawning {} initial bots...", count);
let reduced_count = count.min(20);
let team_spawn_areas = MapGenerator::get_team_spawn_areas();
let mut rng = rand::thread_rng();
for i in 0..reduced_count {
let bot_name_num = self.bot_name_counter.fetch_add(1, AtomicOrdering::SeqCst);
let bot_names = ["Alpha", "Beta", "Gamma", "Delta", "Echo", "Foxtrot", "Golf", "Hotel", "India", "Juliet", "Kilo", "Lima", "Mike", "November", "Oscar", "Papa", "Quebec", "Romeo", "Sierra", "Tango", "Uniform", "Victor", "Whiskey", "Xray", "Yankee", "Zulu"];
let bot_name = format!("Bot {}", bot_names.get(bot_name_num as usize % bot_names.len()).unwrap_or(&"X"));
let bot_player_id_str = format!("bot_{}", Uuid::new_v4());
let team_id = (i % 2) + 1;
let potential_spawns_for_team: Vec<Vec2> = team_spawn_areas.iter()
.filter(|(_, sp_team_id)| *sp_team_id == team_id as u8)
.map(|(pos, _)| *pos)
.collect();
let spawn_pos = {
let center_x = 0.0; let center_y = 0.0;
let spread_radius = 500.0; let angle = rng.gen_range(0.0..2.0 * std::f32::consts::PI);
let distance = rng.gen_range(0.0..spread_radius);
Vec2::new(
center_x + distance * angle.cos(),
center_y + distance * angle.sin()
)
};
if let Some(player_id_arc) = self.player_manager.add_player(bot_player_id_str.clone(), bot_name.clone(), spawn_pos.x, spawn_pos.y) {
if let Some(mut p_state) = self.player_manager.get_player_state_mut(&player_id_arc) {
p_state.team_id = team_id as u8;
}
let bot_controller = BotController {
player_id: player_id_arc.clone(),
target_position: None,
target_enemy_id: None,
last_decision_time: Instant::now(),
behavior_state: BotBehaviorState::Idle,
current_path: VecDeque::new(),
path_recalculation_timer: Instant::now(),
};
self.bot_players.insert(player_id_arc, bot_controller);
debug!("Spawned bot: {} (ID: {}) on team {} at ({:.1}, {:.1})", bot_name, bot_player_id_str, team_id, spawn_pos.x, spawn_pos.y);
} else {
error!("Failed to add bot {} to player manager.", bot_name);
}
}
}
fn apply_input_to_player_state(&self, player_state: &mut PlayerState, input: &PlayerInputData, current_server_time: Instant) {
if !player_state.alive {
player_state.velocity_x = 0.0;
player_state.velocity_y = 0.0;
return;
}
if input.sequence <= player_state.last_processed_input_sequence && input.sequence != 0 {
return;
}
player_state.last_processed_input_sequence = input.sequence;
player_state.mark_field_changed(FIELD_POSITION_ROTATION);
let mut move_x_intent = 0.0_f32;
let mut move_y_intent = 0.0_f32;
if input.move_forward { move_y_intent -= 1.0; }
if input.move_backward { move_y_intent += 1.0; }
if input.move_left { move_x_intent -= 1.0; }
if input.move_right { move_x_intent += 1.0; }
let effective_speed = if player_state.speed_boost_remaining > 0.0 { PLAYER_BASE_SPEED * MAX_PLAYER_SPEED_MULTIPLIER } else { PLAYER_BASE_SPEED };
if move_x_intent != 0.0 || move_y_intent != 0.0 {
let move_magnitude = (move_x_intent * move_x_intent + move_y_intent * move_y_intent).sqrt();
player_state.velocity_x = (move_x_intent / move_magnitude) * effective_speed;
player_state.velocity_y = (move_y_intent / move_magnitude) * effective_speed;
} else {
player_state.velocity_x = 0.0;
player_state.velocity_y = 0.0;
}
player_state.mark_field_changed(FIELD_POSITION_ROTATION);
if (input.rotation - player_state.rotation).abs() > 0.001 {
player_state.rotation = input.rotation;
player_state.mark_field_changed(FIELD_POSITION_ROTATION);
}
if input.shooting && player_state.weapon != ServerWeaponType::Melee && player_state.can_shoot(current_server_time) {
player_state.last_shot_time = Some(current_server_time);
player_state.ammo -= 1;
player_state.mark_field_changed(FIELD_WEAPON_AMMO);
let spawn_offset = PLAYER_RADIUS + 5.0;
let proj_spawn_x = player_state.x + player_state.rotation.cos() * spawn_offset;
let proj_spawn_y = player_state.y + player_state.rotation.sin() * spawn_offset;
let damage_multiplier = if player_state.damage_boost_remaining > 0.0 { 1.5 } else { 1.0 };
self.global_game_events.push(
GameEvent::WeaponFired { player_id: player_state.id.clone(), weapon: player_state.weapon, position: Vec2{x: proj_spawn_x, y: proj_spawn_y}},
EventPriority::Normal
);
match player_state.weapon {
ServerWeaponType::Shotgun => {
for _ in 0..SHOTGUN_PELLET_COUNT { let angle_offset = SHOTGUN_SPREAD_ANGLE_RAD * (2.0 * (rand::random::<f32>()) - 1.0); let dir_x = player_state.rotation.cos() * angle_offset.cos() - player_state.rotation.sin() * angle_offset.sin();
let dir_y = player_state.rotation.sin() * angle_offset.cos() + player_state.rotation.cos() * angle_offset.sin();
self.projectiles_to_add.push(Projectile::new(
player_state.id.clone(),
player_state.weapon,
proj_spawn_x, proj_spawn_y,
dir_x, dir_y,
damage_multiplier,
));
}
}
_ => { self.projectiles_to_add.push(Projectile::new(
player_state.id.clone(),
player_state.weapon,
proj_spawn_x, proj_spawn_y,
player_state.rotation.cos(), player_state.rotation.sin(),
damage_multiplier,
));
}
}
}
if input.melee_attack && player_state.can_shoot(current_server_time) { player_state.last_shot_time = Some(current_server_time);
let melee_event_pos_x = player_state.x + player_state.rotation.cos() * (PLAYER_RADIUS + 1.0);
let melee_event_pos_y = player_state.y + player_state.rotation.sin() * (PLAYER_RADIUS + 1.0);
debug!("[{}] initiated Melee Attack (V key).", player_state.id);
self.global_game_events.push(
GameEvent::MeleeHit {
attacker_id: player_state.id.clone(),
target_id: None, position: Vec2 { x: melee_event_pos_x, y: melee_event_pos_y }
},
EventPriority::Normal
);
}
if input.reload {
player_state.start_reload(current_server_time);
}
if input.change_weapon_slot != 0 {
let new_weapon = match input.change_weapon_slot {
1 => Some(ServerWeaponType::Pistol),
2 => Some(ServerWeaponType::Shotgun),
3 => Some(ServerWeaponType::Rifle),
4 => Some(ServerWeaponType::Sniper),
5 => Some(ServerWeaponType::Melee),
_ => None,
};
if let Some(weapon) = new_weapon {
if player_state.weapon != weapon {
player_state.weapon = weapon;
player_state.ammo = PlayerState::get_max_ammo_for_weapon(weapon);
player_state.reload_progress = None;
player_state.mark_field_changed(FIELD_WEAPON_AMMO);
}
}
}
}
pub async fn process_network_input(&self) {
let current_server_time = Instant::now();
self.player_manager.for_each_player_mut(|_player_id, player_state| {
player_state.clear_changed_fields();
let inputs_to_process: Vec<PlayerInputData> = player_state.input_queue.drain(..).collect();
for input in inputs_to_process {
self.apply_input_to_player_state(player_state, &input, current_server_time);
}
});
}
pub async fn run_ai_update(&self) {
let delta_time = TICK_DURATION.as_secs_f32();
if true { BotAISystem::update_bots(self, delta_time);
}
}
pub async fn run_physics_update(&self, delta_time: f32) {
let physics_start_time = Instant::now();
let frame = self.frame_counter.load(AtomicOrdering::Relaxed);
let respawn_stage_start = Instant::now();
let _respawned_walls = if frame % 30 == 0 { let templates = self.wall_respawn_manager.as_ref().check_respawns(); if !templates.is_empty() {
debug!("[Frame {}]: Respawning {} walls (took {:?})", frame, templates.len(), respawn_stage_start.elapsed());
self.process_wall_respawns(templates).await } else { Vec::new() }
} else { Vec::new() };
let collect_walls_start = Instant::now();
let active_walls = self.get_active_walls_cached(frame).await; debug!("Frame {}: Collected {} active walls (took {:?})", frame, active_walls.len(), collect_walls_start.elapsed());
let player_physics_start = Instant::now();
let player_updates = self.process_player_physics_parallel(&active_walls, delta_time).await; debug!("Frame {}: Processed {} player physics updates (took {:?})", frame, player_updates.players_to_respawn.len() + player_updates.alive_count, player_physics_start.elapsed());
let apply_updates_start = Instant::now();
self.apply_player_updates(player_updates).await; debug!("Frame {}: Applied player updates (took {:?})", frame, apply_updates_start.elapsed());
let projectiles_start = Instant::now();
let projectile_results = self.process_projectiles_optimized(&active_walls, delta_time).await; debug!("Frame {}: Processed {} projectiles, {} hits, {} removed (took {:?})", frame, projectile_results.total_processed, projectile_results.hits.len(), projectile_results.to_remove.len(), projectiles_start.elapsed());
let apply_projectiles_start = Instant::now();
self.apply_projectile_results(projectile_results).await; debug!("Frame {}: Applied projectile results (took {:?})", frame, apply_projectiles_start.elapsed());
let pickups_start = Instant::now();
self.process_pickup_respawns(delta_time).await; debug!("Frame {}: Processed pickups (took {:?})", frame, pickups_start.elapsed());
debug!("Frame {}: TOTAL physics update took {:?}", frame, physics_start_time.elapsed());
}
async fn process_wall_respawns(&self, templates: Vec<Wall>) -> Vec<EntityId> {
let mut updated_walls_guard = self.updated_walls_this_tick.write();
let mut respawned_ids = Vec::with_capacity(templates.len());
for wall_template in templates {
let partition_idx = self.world_partition_manager.get_partition_index_for_point(
wall_template.x + wall_template.width / 2.0,
wall_template.y + wall_template.height / 2.0
);
if let Some(partition) = self.world_partition_manager.get_partition(partition_idx) {
if partition.respawn_destructible_wall(wall_template.id) {
if let Some(respawned_wall_state) = partition.get_wall(wall_template.id) {
updated_walls_guard.insert(wall_template.id, respawned_wall_state);
respawned_ids.push(wall_template.id);
}
}
}
}
respawned_ids
}
async fn get_active_walls_cached(&self, frame: u64) -> Arc<Vec<Wall>> {
static WALL_CACHE: OnceCell<Arc<ParkingLotRwLock<(u64, Arc<Vec<Wall>>)>>> = OnceCell::new();
let cache = WALL_CACHE.get_or_init(|| Arc::new(ParkingLotRwLock::new((0, Arc::new(Vec::new())))));
let cache_read = cache.read();
if cache_read.0 + 5 > frame { return cache_read.1.clone();
}
drop(cache_read);
let mut cache_write = cache.write();
if cache_write.0 + 5 > frame { return cache_write.1.clone();
}
let walls = Arc::new(self.collect_active_walls_optimized());
cache_write.0 = frame;
cache_write.1 = walls.clone();
walls
}
fn collect_active_walls_optimized(&self) -> Vec<Wall> {
let frame = self.frame_counter.load(AtomicOrdering::Relaxed);
let cache_entry_arc = CACHED_WALLS.get().expect("Wall cache should have been initialized in MassiveGameServer::new()");
let structural_walls_from_cache = { let guard = cache_entry_arc.read();
if guard.0 == frame || (guard.0 != u64::MAX && guard.0 >= frame.saturating_sub(10)) {
debug!("[Frame {}] Using cached structural walls (cache frame {}, count {}).", frame, guard.0, guard.1.len());
guard.1.clone()
} else {
drop(guard); let mut write_guard = cache_entry_arc.write();
if write_guard.0 == frame || (write_guard.0 != u64::MAX && write_guard.0 >= frame.saturating_sub(10)) {
debug!("[Frame {}] Cache updated by another thread. Using new structural walls.", frame);
write_guard.1.clone()
} else {
info!("[Frame {}] Rebuilding structural wall cache (was for frame {}).", frame, write_guard.0);
let mut new_cache_walls = Vec::new();
let partitions = self.world_partition_manager.get_partitions_for_processing();
for partition in &partitions {
for entry in partition.all_walls_in_partition.iter() {
new_cache_walls.push(entry.value().clone());
}
}
info!("[Frame {}] Structural wall cache rebuilt with {} walls.", frame, new_cache_walls.len());
write_guard.0 = frame;
write_guard.1 = new_cache_walls.clone();
new_cache_walls
}
}
};
let active_walls = structural_walls_from_cache.into_iter()
.filter(|wall| !wall.is_destructible || (wall.is_destructible && wall.current_health > 0))
.collect::<Vec<Wall>>();
debug!("[Frame {}] Collected {} active walls.", frame, active_walls.len());
active_walls
}
fn update_client_state_after_initial(
&self, peer_id_str: &str,
shared_data: &SharedBroadcastData,
) {
let frame_num = self.frame_counter.load(AtomicOrdering::Relaxed);
trace!("[Frame {}] Client {}: Preparing to set initial ClientState in DashMap.", frame_num, peer_id_str);
let mut client_state = ClientState::default(); client_state.known_walls_sent = true; client_state.last_update_sent_time = Instant::now();
client_state.last_known_match_state = Some(shared_data.match_info_snapshot.match_state);
client_state.last_known_match_time_remaining = Some(shared_data.match_info_snapshot.time_remaining);
client_state.last_known_team_scores = shared_data.match_info_snapshot.team_scores.clone();
let self_player_id_arc = self.player_manager.id_pool.get_or_create(peer_id_str);
if let Some(self_pstate_guard) = self.player_manager.get_player_state(&self_player_id_arc) {
client_state.last_known_player_states.insert(self_player_id_arc.clone(), (*self_pstate_guard).clone());
}
if let Some(aoi_entry) = self.player_aois.get(peer_id_str) {
let p_aoi = aoi_entry.value();
for visible_player_id in p_aoi.visible_players.iter() {
if let Some(pstate_guard) = self.player_manager.get_player_state(visible_player_id) {
client_state.last_known_player_states.insert(visible_player_id.clone(), (*pstate_guard).clone());
}
}
client_state.last_known_projectile_ids = p_aoi.visible_projectiles.clone();
let pickups_guard = self.pickups.read();
for pickup_id in p_aoi.visible_pickups.iter() {
if let Some(pickup) = pickups_guard.iter().find(|p| p.id == *pickup_id) {
client_state.last_known_pickup_states.insert(*pickup_id, PickupState { is_active: pickup.is_active });
}
}
}
let key_for_insert = peer_id_str.to_string();
trace!("[Frame {}] Client {}: ABOUT TO INSERT initial ClientState into client_states_map. Key: {}", frame_num, peer_id_str, key_for_insert);
self.client_states_map.write().insert(key_for_insert.clone(), client_state);
trace!("[Frame {}] Client {}: SUCCESSFULLY INSERTED initial ClientState into client_states_map. Key: {}", frame_num, peer_id_str, key_for_insert);
}
pub(crate) async fn send_chat_messages_static(
_peer_id_str: &str,
data_channel: &Arc<crate::core::types::RTCDataChannel>,
client_state: &mut ClientState,
chat_messages: &[ChatMessage],
_chat_messages_queue: &ChatMessagesQueue,
) {
let last_seq_sent = client_state.last_chat_message_seq_sent;
let mut max_seq_in_batch = last_seq_sent;
let messages_to_send: Vec<&ChatMessage> = chat_messages
.iter()
.filter(|msg| msg.seq > last_seq_sent)
.take(10) .collect();
for chat_entry in messages_to_send {
let mut chat_builder = flatbuffers::FlatBufferBuilder::with_capacity(256);
let player_id_fb = chat_builder.create_string(chat_entry.player_id.as_str());
let username_fb = chat_builder.create_string(&chat_entry.username);
let message_fb = chat_builder.create_string(&chat_entry.message);
let chat_payload_offset = fb::ChatMessage::create(&mut chat_builder, &fb::ChatMessageArgs {
seq: chat_entry.seq,
player_id: Some(player_id_fb),
username: Some(username_fb),
message: Some(message_fb),
timestamp: chat_entry.timestamp,
});
let game_message_offset = fb::GameMessage::create(&mut chat_builder, &fb::GameMessageArgs {
msg_type: fb::MessageType::Chat,
actual_message_type: fb::MessagePayload::ChatMessage,
actual_message: Some(chat_payload_offset.as_union_value()),
});
chat_builder.finish(game_message_offset, None);
let chat_msg_bytes = Bytes::from(chat_builder.finished_data().to_vec());
let _ = data_channel.send(&chat_msg_bytes).await;
if chat_entry.seq > max_seq_in_batch {
max_seq_in_batch = chat_entry.seq;
}
}
client_state.last_chat_message_seq_sent = max_seq_in_batch;
}
fn update_client_state_after_delta(
&self,
client_state: &mut ClientState,
player_id: &PlayerID,
) {
let player_aoi = match self.player_aois.get(player_id.as_str()) {
Some(aoi_entry) => aoi_entry.clone(),
None => {
debug!("No AoI found for player {} when updating client state", player_id.as_str());
return;
}
};
client_state.last_broadcast_frame = self.frame_counter.load(AtomicOrdering::Relaxed);
client_state.last_known_projectile_ids.clear();
for projectile_id in &player_aoi.visible_projectiles {
client_state.last_known_projectile_ids.insert(*projectile_id);
}
client_state.last_known_pickup_states.clear();
let pickups_guard = self.pickups.read();
for pickup_id in &player_aoi.visible_pickups {
if let Some(pickup) = pickups_guard.iter().find(|p| p.id == *pickup_id) {
client_state.last_known_pickup_states.insert(
*pickup_id,
PickupState {
is_active: pickup.is_active,
}
);
}
}
drop(pickups_guard);
client_state.last_known_players.clear();
for visible_player_id in &player_aoi.visible_players {
client_state.last_known_players.insert(visible_player_id.clone());
}
if let Some(ref mut last_known_walls) = client_state.last_known_wall_ids {
last_known_walls.clear();
for wall_id in &player_aoi.visible_walls {
last_known_walls.insert(*wall_id);
}
}
trace!(
"Updated client state for {}: {} projectiles, {} pickups, {} players tracked",
player_id.as_str(),
client_state.last_known_projectile_ids.len(),
client_state.last_known_pickup_states.len(),
client_state.last_known_players.len()
);
}
fn update_client_state_after_delta_static(
peer_id_str: &str,
mut client_state: ClientState,
shared_data: &SharedBroadcastData,
client_states_map: &ClientStatesMap,
frame_num: u64,
) {
trace!("[Frame {}] Client {}: Preparing to update ClientState in RwLock<HashMap> (delta). Last seq sent: {}",
frame_num, peer_id_str, client_state.last_chat_message_seq_sent);
client_state.last_update_sent_time = Instant::now();
for wall_id in &shared_data.destroyed_wall_ids {
client_state.known_destroyed_wall_ids.insert(*wall_id);
}
client_state.last_known_match_state = Some(shared_data.match_info_snapshot.match_state);
client_state.last_known_match_time_remaining = Some(shared_data.match_info_snapshot.time_remaining);
client_state.last_known_team_scores = shared_data.match_info_snapshot.team_scores.clone();
client_state.last_kill_feed_count_sent = shared_data.kill_feed_snapshot.len();
let key_for_insert = peer_id_str.to_string();
trace!("[Frame {}] Client {}: ABOUT TO INSERT ClientState into RwLock<HashMap>. Key: {}",
frame_num, peer_id_str, key_for_insert);
client_states_map.write().insert(key_for_insert.clone(), client_state);
trace!("[Frame {}] Client {}: SUCCESSFULLY INSERTED ClientState into RwLock<HashMap>. Key: {}",
frame_num, peer_id_str, key_for_insert);
}
fn get_empty_player_aoi() -> PlayerAoI {
PlayerAoI {
visible_players: HashSet::new(),
visible_projectiles: HashSet::new(),
visible_pickups: HashSet::new(),
visible_walls: HashSet::new(),
last_update: Instant::now(), }
}
async fn process_player_physics_parallel(&self, walls: &[Wall], delta_time: f32) -> PlayerPhysicsResults {
let wall_arc = Arc::new(walls.to_vec());
let mut all_to_respawn = Vec::new();
let mut total_alive = 0;
self.player_manager.for_each_player_mut(|player_id, player_state| {
player_state.update_timers(delta_time);
if player_state.alive {
total_alive += 1;
self.process_player_movement_optimized(player_state, &wall_arc, delta_time);
} else if player_state.respawn_timer == Some(0.0) {
all_to_respawn.push((player_id.clone(), player_state.team_id));
}
});
PlayerPhysicsResults {
players_to_respawn: all_to_respawn,
alive_count: total_alive,
}
}
fn process_player_movement_optimized(&self, player_state: &mut PlayerState, walls: &[Wall], delta_time: f32) {
let old_x = player_state.x;
let old_y = player_state.y;
player_state.x += player_state.velocity_x * delta_time;
player_state.y += player_state.velocity_y * delta_time;
let half_radius = PLAYER_RADIUS;
if player_state.x < WORLD_MIN_X + half_radius ||
player_state.x > WORLD_MAX_X - half_radius ||
player_state.y < WORLD_MIN_Y + half_radius ||
player_state.y > WORLD_MAX_Y - half_radius {
player_state.x = player_state.x.clamp(WORLD_MIN_X + half_radius, WORLD_MAX_X - half_radius);
player_state.y = player_state.y.clamp(WORLD_MIN_Y + half_radius, WORLD_MAX_Y - half_radius);
player_state.velocity_x = 0.0;
player_state.velocity_y = 0.0;
player_state.mark_field_changed(FIELD_POSITION_ROTATION);
return;
}
let check_radius = PLAYER_RADIUS + 50.0;
let player_bounds = (
player_state.x - check_radius,
player_state.y - check_radius,
player_state.x + check_radius,
player_state.y + check_radius
);
for wall in walls.iter().filter(|w|
w.x < player_bounds.2 &&
w.x + w.width > player_bounds.0 &&
w.y < player_bounds.3 &&
w.y + w.height > player_bounds.1
) {
let closest_x = player_state.x.clamp(wall.x, wall.x + wall.width);
let closest_y = player_state.y.clamp(wall.y, wall.y + wall.height);
let dist_sq = (player_state.x - closest_x).powi(2) + (player_state.y - closest_y).powi(2);
if dist_sq < PLAYER_RADIUS.powi(2) {
player_state.x = old_x;
player_state.y = old_y;
player_state.velocity_x = 0.0;
player_state.velocity_y = 0.0;
player_state.mark_field_changed(FIELD_POSITION_ROTATION);
return;
}
}
let max_dist = PLAYER_BASE_SPEED * MAX_PLAYER_SPEED_MULTIPLIER * delta_time + MAX_POSITION_DELTA_SLACK;
let actual_dist = ((player_state.x - player_state.last_valid_position.0).powi(2) +
(player_state.y - player_state.last_valid_position.1).powi(2)).sqrt();
if actual_dist > max_dist {
player_state.violation_count += 1;
if player_state.violation_count > POSITION_VALIDATION_VIOLATION_THRESHOLD {
player_state.x = player_state.last_valid_position.0;
player_state.y = player_state.last_valid_position.1;
player_state.velocity_x = 0.0;
player_state.velocity_y = 0.0;
player_state.mark_field_changed(FIELD_POSITION_ROTATION);
}
} else {
player_state.last_valid_position = (player_state.x, player_state.y);
player_state.violation_count = player_state.violation_count.saturating_sub(1);
}
if (old_x - player_state.x).abs() > 0.01 || (old_y - player_state.y).abs() > 0.01 {
player_state.mark_field_changed(FIELD_POSITION_ROTATION);
}
}
async fn apply_player_updates(&self, updates: PlayerPhysicsResults) {
for (player_id, team_id) in updates.players_to_respawn {
let enemies = self.get_enemy_positions_for_team(team_id);
let spawn_pos = self.respawn_manager.get_respawn_position(
self,
&player_id,
Some(team_id),
&enemies
);
if let Some(mut p_state) = self.player_manager.get_player_state_mut(&player_id) {
p_state.respawn(spawn_pos.x, spawn_pos.y);
self.global_game_events.push(
GameEvent::PlayerJoined { player_id: player_id.clone() },
EventPriority::High
);
}
}
}
async fn process_projectiles_optimized(&self, walls: &[Wall], delta_time: f32) -> ProjectileResults {
use rayon::prelude::*;
use std::sync::Mutex;
let frame = self.frame_counter.load(AtomicOrdering::Relaxed);
trace!("[Frame {}] Starting optimized projectile processing", frame);
let mut all_projectiles = {
let mut guard = self.projectiles.write();
std::mem::take(&mut *guard)
};
let total_projectiles = all_projectiles.len();
trace!("[Frame {}] Processing {} projectiles", frame, total_projectiles);
let hits = Arc::new(Mutex::new(Vec::new()));
let wall_hits = Arc::new(Mutex::new(Vec::new()));
let spatial_updates = Arc::new(Mutex::new(Vec::new()));
let chunk_size = 50.max(total_projectiles / rayon::current_num_threads());
let to_remove: Vec<usize> = all_projectiles
.par_chunks_mut(chunk_size)
.enumerate()
.flat_map(|(chunk_idx, chunk)| {
let mut chunk_to_remove = Vec::new();
let chunk_start_idx = chunk_idx * chunk_size;
for (local_idx, proj) in chunk.iter_mut().enumerate() {
let global_idx = chunk_start_idx + local_idx;
let old_x = proj.x;
let old_y = proj.y;
proj.x += proj.velocity_x * delta_time;
proj.y += proj.velocity_y * delta_time;
spatial_updates.lock().unwrap().push((proj.id, proj.x, proj.y));
if proj.x < WORLD_MIN_X || proj.x > WORLD_MAX_X ||
proj.y < WORLD_MIN_Y || proj.y > WORLD_MAX_Y {
chunk_to_remove.push(global_idx);
continue;
}
if proj.should_remove() {
chunk_to_remove.push(global_idx);
continue;
}
let partition_idx = self.world_partition_manager.get_partition_index_for_point(proj.x, proj.y);
if let Some(partition) = self.world_partition_manager.get_partition(partition_idx) {
let mut hit_wall = false;
let ray_length = ((proj.x - old_x).powi(2) + (proj.y - old_y).powi(2)).sqrt();
let ray_steps = (ray_length / 5.0).ceil() as usize;
'wall_check: for step in 0..=ray_steps {
let t = step as f32 / ray_steps.max(1) as f32;
let check_x = old_x + (proj.x - old_x) * t;
let check_y = old_y + (proj.y - old_y) * t;
for wall_entry in partition.all_walls_in_partition.iter() {
let wall = wall_entry.value();
if wall.is_destructible && wall.current_health <= 0 {
continue;
}
if check_x >= wall.x && check_x <= wall.x + wall.width &&
check_y >= wall.y && check_y <= wall.y + wall.height {
proj.x = check_x;
proj.y = check_y;
if wall.is_destructible {
wall_hits.lock().unwrap().push((wall.id, proj.damage));
self.global_game_events.push(
GameEvent::WallImpact {
position: Vec2::new(check_x, check_y),
wall_id: wall.id,
damage: proj.damage,
},
EventPriority::Normal
);
}
chunk_to_remove.push(global_idx);
hit_wall = true;
break 'wall_check;
}
}
}
if !hit_wall {
let nearby_players = self.spatial_index.query_nearby_players(
proj.x,
proj.y,
PLAYER_RADIUS + 20.0 );
for target_id in nearby_players {
if target_id == proj.owner_id {
continue; }
if let Some(target_state) = self.player_manager.get_player_state(&target_id) {
if !target_state.alive {
continue;
}
let mut hit = false;
for step in 0..=ray_steps {
let t = step as f32 / ray_steps.max(1) as f32;
let check_x = old_x + (proj.x - old_x) * t;
let check_y = old_y + (proj.y - old_y) * t;
let dx = target_state.x - check_x;
let dy = target_state.y - check_y;
let dist_sq = dx * dx + dy * dy;
if dist_sq <= PLAYER_RADIUS * PLAYER_RADIUS {
proj.x = check_x;
proj.y = check_y;
hit = true;
break;
}
}
if hit {
hits.lock().unwrap().push((
proj.owner_id.clone(),
target_id.clone(),
proj.damage,
proj.weapon_type
));
chunk_to_remove.push(global_idx);
break;
}
}
}
}
}
}
chunk_to_remove
})
.collect();
let spatial_updates_vec = spatial_updates.lock().unwrap().clone();
self.spatial_index.batch_update_projectiles(&spatial_updates_vec);
let to_remove_set: HashSet<_> = to_remove.into_iter().collect();
let mut kept_projectiles = Vec::with_capacity(all_projectiles.len());
let mut removed_ids = Vec::new();
for (idx, proj) in all_projectiles.into_iter().enumerate() {
if to_remove_set.contains(&idx) {
removed_ids.push(proj.id);
} else {
kept_projectiles.push(proj);
}
}
for proj_id in &removed_ids {
self.spatial_index.remove_projectile(proj_id);
}
*self.projectiles.write() = kept_projectiles;
let wall_hits_vec = wall_hits.lock().unwrap().clone();
for (wall_id, damage) in &wall_hits_vec {
let partition_idx = self.world_partition_manager
.get_partitions_for_processing()
.iter()
.position(|p| p.all_walls_in_partition.contains_key(wall_id));
if let Some(idx) = partition_idx {
if let Some(partition) = self.world_partition_manager.get_partition(idx) {
if let Some((destroyed, pos)) = partition.damage_destructible_wall(*wall_id, *damage) {
if destroyed {
self.global_game_events.push(
GameEvent::WallDestroyed {
wall_id: *wall_id,
position: pos,
},
EventPriority::High
);
self.destroyed_wall_ids_this_tick.write().insert(*wall_id);
self.wall_respawn_manager.wall_destroyed(*wall_id);
}
}
}
}
}
let hits_vec = hits.lock().unwrap().clone();
trace!(
"[Frame {}] Projectile processing complete: {} processed, {} hits, {} wall hits, {} removed",
frame, total_projectiles, hits_vec.len(), wall_hits_vec.len(), removed_ids.len()
);
ProjectileResults {
total_processed: total_projectiles,
hits: hits_vec,
wall_hits: wall_hits_vec,
to_remove: Vec::new(), }
}
async fn apply_projectile_results(&self, results: ProjectileResults) {
for (attacker_id, target_id, damage, weapon) in results.hits {
if let Some(mut target_state_entry) = self.player_manager.get_player_state_mut(&target_id) {
if target_state_entry.alive {
let died = target_state_entry.apply_damage(damage);
let target_pos = Vec2::new(target_state_entry.x, target_state_entry.y);
self.global_game_events.push(GameEvent::PlayerDamaged {
target_id: target_id.clone(),
attacker_id: Some(attacker_id.clone()),
damage,
weapon,
position: target_pos,
}, EventPriority::Normal);
if died {
if attacker_id != target_id {
if let Some(mut attacker_state_entry) = self.player_manager.get_player_state_mut(&attacker_id) {
attacker_state_entry.kills += 1;
attacker_state_entry.score += 100;
attacker_state_entry.mark_field_changed(FIELD_SCORE_STATS);
}
}
self.global_game_events.push(GameEvent::PlayerKilled {
victim_id: target_id.clone(),
killer_id: attacker_id.clone(),
weapon,
position: target_pos,
}, EventPriority::High);
let mut kill_feed_guard = self.kill_feed.write();
kill_feed_guard.push_back(ServerKillFeedEntry {
killer_name: self.player_manager.get_player_state(&attacker_id)
.map_or_else(|| "World".to_string(), |p| p.username.clone()),
victim_name: target_state_entry.username.clone(),
weapon,
timestamp: self.frame_counter.load(AtomicOrdering::Relaxed),
});
if kill_feed_guard.len() > MAX_KILL_FEED_HISTORY {
kill_feed_guard.pop_front();
}
}
}
}
}
}
async fn process_pickup_respawns(&self, delta_time: f32) {
let mut pickups_guard = self.pickups.write();
for pickup in pickups_guard.iter_mut() {
if !pickup.is_active {
if let Some(timer) = &mut pickup.respawn_timer {
*timer -= delta_time;
if *timer <= 0.0 {
pickup.is_active = true;
pickup.respawn_timer = None;
}
}
}
}
}
fn get_enemy_positions_for_team(&self, team_id: u8) -> Vec<(Vec2, PlayerID)> {
let mut enemies = Vec::with_capacity(50);
self.player_manager.for_each_player(|id, state| {
if state.alive && state.team_id != team_id && state.team_id != 0 {
enemies.push((Vec2::new(state.x, state.y), id.clone()));
}
});
enemies
}
pub fn collect_all_walls_current_state(&self) -> Vec<Wall> {
let mut all_walls = Vec::new();
for partition_arc in self.world_partition_manager.get_partitions_for_processing() {
partition_arc.all_walls_in_partition.iter().for_each(|wall_entry| {
let wall = wall_entry.value();
all_walls.push(wall.clone());
});
}
all_walls
}
pub async fn run_game_logic_update(&self, delta_time: f32) {
while let Some(proj) = self.projectiles_to_add.pop() {
self.projectiles.write().push(proj);
}
{
let mut match_info_guard = self.match_info.write();
let player_count = self.player_manager.player_count();
match match_info_guard.match_state {
fb::MatchStateType::Waiting => {
if player_count >= MIN_PLAYERS_TO_START {
match_info_guard.match_state = fb::MatchStateType::Active;
match_info_guard.time_remaining = 300.0;
info!("Match starting! Mode: {:?}", match_info_guard.game_mode);
if match_info_guard.game_mode == fb::GameModeType::CaptureTheFlag {
self.initialize_ctf_flags(&mut match_info_guard);
}
self.player_manager.for_each_player_mut(|_id, p_state| {
p_state.score = 0;
p_state.kills = 0;
p_state.deaths = 0;
p_state.is_carrying_flag_team_id = 0;
p_state.mark_field_changed(FIELD_SCORE_STATS | FIELD_FLAG);
});
self.kill_feed.write().clear();
}
}
fb::MatchStateType::Active => {
match_info_guard.time_remaining -= delta_time;
if match_info_guard.time_remaining <= 0.0 {
match_info_guard.match_state = fb::MatchStateType::Ended;
info!("Match ended! (Time up)");
if match_info_guard.game_mode == fb::GameModeType::TeamDeathmatch || match_info_guard.game_mode == fb::GameModeType::CaptureTheFlag {
let _team1_score = match_info_guard.team_scores.get(&1).cloned().unwrap_or(0);
let _team2_score = match_info_guard.team_scores.get(&2).cloned().unwrap_or(0);
}
}
}
fb::MatchStateType::Ended => {
match_info_guard.time_remaining -= delta_time;
if match_info_guard.time_remaining <= -10.0 {
match_info_guard.match_state = fb::MatchStateType::Waiting;
self.reset_match_state(&mut match_info_guard);
info!("Match reset to Waiting.");
}
}
_ => {}
}
}
self.player_manager.for_each_player_mut(|player_id_arc_for_pickup, player_state_for_pickup| {
if !player_state_for_pickup.alive { return; }
let mut pickups_guard = self.pickups.write();
for pickup_idx in 0..pickups_guard.len() {
if pickups_guard[pickup_idx].is_active {
let pickup_ref = &pickups_guard[pickup_idx];
let dx = player_state_for_pickup.x - pickup_ref.x;
let dy = player_state_for_pickup.y - pickup_ref.y;
if (dx * dx + dy * dy) < (PICKUP_COLLECTION_RADIUS * PICKUP_COLLECTION_RADIUS) {
let mut collected = false;
let pickup_pos = Vec2::new(pickup_ref.x, pickup_ref.y);
let pickup_id_event = pickup_ref.id;
let pickup_type_event = pickup_ref.pickup_type.clone();
let mut_pickup = &mut pickups_guard[pickup_idx];
match &mut_pickup.pickup_type {
CorePickupType::Health => {
if player_state_for_pickup.health < player_state_for_pickup.max_health {
player_state_for_pickup.health = (player_state_for_pickup.health + 50).min(player_state_for_pickup.max_health);
player_state_for_pickup.mark_field_changed(FIELD_HEALTH_ALIVE);
collected = true;
}
}
CorePickupType::Ammo => {
player_state_for_pickup.ammo = PlayerState::get_max_ammo_for_weapon(player_state_for_pickup.weapon);
player_state_for_pickup.mark_field_changed(FIELD_WEAPON_AMMO);
collected = true;
}
CorePickupType::WeaponCrate(weapon) => {
player_state_for_pickup.weapon = *weapon;
player_state_for_pickup.ammo = PlayerState::get_max_ammo_for_weapon(*weapon);
player_state_for_pickup.reload_progress = None;
player_state_for_pickup.mark_field_changed(FIELD_WEAPON_AMMO);
collected = true;
}
CorePickupType::SpeedBoost => {
player_state_for_pickup.speed_boost_remaining = 10.0;
player_state_for_pickup.mark_field_changed(FIELD_POWERUPS);
collected = true;
}
CorePickupType::DamageBoost => {
player_state_for_pickup.damage_boost_remaining = 10.0;
player_state_for_pickup.mark_field_changed(FIELD_POWERUPS);
collected = true;
}
CorePickupType::Shield => {
player_state_for_pickup.shield_max = 50;
player_state_for_pickup.shield_current = player_state_for_pickup.shield_max;
player_state_for_pickup.mark_field_changed(FIELD_SHIELD);
collected = true;
}
}
if collected {
mut_pickup.is_active = false;
mut_pickup.respawn_timer = Some(mut_pickup.get_respawn_duration());
self.global_game_events.push(GameEvent::PowerupCollected {
player_id: player_id_arc_for_pickup.clone(),
pickup_id: pickup_id_event,
pickup_type: pickup_type_event,
position: pickup_pos,
}, EventPriority::Normal);
break;
}
}
}
}
});
let mut match_info_write_guard = self.match_info.write();
if match_info_write_guard.game_mode == fb::GameModeType::CaptureTheFlag && match_info_write_guard.match_state == fb::MatchStateType::Active {
for flag_state in match_info_write_guard.flag_states.values_mut() {
if flag_state.status == fb::FlagStatus::Dropped && flag_state.respawn_timer > 0.0 {
flag_state.respawn_timer -= delta_time;
if flag_state.respawn_timer <= 0.0 {
flag_state.respawn_timer = 0.0;
flag_state.status = fb::FlagStatus::AtBase;
flag_state.position = Self::get_flag_base_position(flag_state.team_id);
flag_state.carrier_id = None;
self.global_game_events.push(GameEvent::FlagReturned {
player_id: Arc::new("server".to_string()),
flag_team_id: flag_state.team_id,
position: flag_state.position
}, EventPriority::High);
info!("Flag of team {} auto-returned to base.", flag_state.team_id);
}
}
}
let mut player_snapshots: HashMap<PlayerID, PlayerState> = HashMap::new();
self.player_manager.for_each_player(|id, state| {
player_snapshots.insert(id.clone(), state.clone());
});
for (player_id_arc, player_state_snapshot) in &player_snapshots {
if !player_state_snapshot.alive { continue; }
if player_state_snapshot.is_carrying_flag_team_id == 0 {
for flag_state in match_info_write_guard.flag_states.values_mut() {
if flag_state.status == fb::FlagStatus::AtBase || (flag_state.status == fb::FlagStatus::Dropped && flag_state.respawn_timer <= 0.0) {
let dx = player_state_snapshot.x - flag_state.position.x;
let dy = player_state_snapshot.y - flag_state.position.y;
if (dx * dx + dy * dy) < (PICKUP_COLLECTION_RADIUS * PICKUP_COLLECTION_RADIUS) {
if flag_state.team_id != player_state_snapshot.team_id {
flag_state.status = fb::FlagStatus::Carried;
flag_state.carrier_id = Some(player_id_arc.clone());
if let Some(mut p_state_mut_entry) = self.player_manager.get_player_state_mut(player_id_arc) {
let p_state_mut = &mut *p_state_mut_entry;
p_state_mut.is_carrying_flag_team_id = flag_state.team_id;
p_state_mut.mark_field_changed(FIELD_FLAG);
}
self.global_game_events.push(GameEvent::FlagGrabbed { player_id: player_id_arc.clone(), flag_team_id: flag_state.team_id, position: flag_state.position }, EventPriority::High);
info!("Player {} grabbed flag of team {}", player_state_snapshot.username, flag_state.team_id);
break;
} else if flag_state.status == fb::FlagStatus::Dropped && flag_state.team_id == player_state_snapshot.team_id {
flag_state.status = fb::FlagStatus::AtBase;
flag_state.position = Self::get_flag_base_position(flag_state.team_id);
flag_state.carrier_id = None;
flag_state.respawn_timer = 0.0;
self.global_game_events.push(GameEvent::FlagReturned { player_id: player_id_arc.clone(), flag_team_id: flag_state.team_id, position: flag_state.position }, EventPriority::High);
info!("Player {} returned own team {}'s flag.", player_state_snapshot.username, flag_state.team_id);
break;
}
}
}
}
}
if player_state_snapshot.is_carrying_flag_team_id != 0 &&
player_state_snapshot.is_carrying_flag_team_id != player_state_snapshot.team_id {
let own_player_team_id = player_state_snapshot.team_id;
let own_flag_at_base = match_info_write_guard.flag_states.get(&own_player_team_id)
.map_or(false, |ofs| ofs.status == fb::FlagStatus::AtBase);
if own_flag_at_base {
let own_flag_base_pos = Self::get_flag_base_position(own_player_team_id);
let dx = player_state_snapshot.x - own_flag_base_pos.x;
let dy = player_state_snapshot.y - own_flag_base_pos.y;
if (dx*dx + dy*dy) < (PICKUP_COLLECTION_RADIUS * PICKUP_COLLECTION_RADIUS) {
let captured_flag_team_id = player_state_snapshot.is_carrying_flag_team_id;
if let Some(captured_flag) = match_info_write_guard.flag_states.get_mut(&captured_flag_team_id) {
captured_flag.status = fb::FlagStatus::AtBase;
captured_flag.position = Self::get_flag_base_position(captured_flag_team_id);
captured_flag.carrier_id = None;
}
if let Some(mut p_state_mut_entry) = self.player_manager.get_player_state_mut(player_id_arc) {
let p_state_mut = &mut *p_state_mut_entry;
p_state_mut.is_carrying_flag_team_id = 0;
p_state_mut.mark_field_changed(FIELD_FLAG);
p_state_mut.score += 100;
p_state_mut.mark_field_changed(FIELD_SCORE_STATS);
}
let team_score_mut_ref = match_info_write_guard.team_scores.entry(own_player_team_id).or_insert(0);
*team_score_mut_ref += 1;
let current_score = *team_score_mut_ref;
self.global_game_events.push(GameEvent::FlagCaptured {
capturer_id: player_id_arc.clone(),
captured_flag_team_id,
capturing_team_id: own_player_team_id,
position: own_flag_base_pos
}, EventPriority::High);
info!("Player {} captured team {}'s flag for team {}! (Score: {})", player_state_snapshot.username, captured_flag_team_id, own_player_team_id, current_score);
if current_score >= 3 {
match_info_write_guard.match_state = fb::MatchStateType::Ended;
info!("Team {} wins by capturing {} flags!", own_player_team_id, current_score);
}
}
}
}
}
}
drop(match_info_write_guard);
let mut melee_hit_events_to_process = Vec::new();
let mut other_events_to_requeue = Vec::new();
while let Some(event_popped) = self.global_game_events.pop() {
if matches!(event_popped, GameEvent::MeleeHit { .. }) {
melee_hit_events_to_process.push(event_popped);
} else {
other_events_to_requeue.push(event_popped);
}
if melee_hit_events_to_process.len() + other_events_to_requeue.len() > 200 {
warn!("Event processing loop safety break triggered in run_game_logic_update.");
break;
}
}
self.process_melee_hits(melee_hit_events_to_process);
for event_to_requeue in other_events_to_requeue {
self.global_game_events.push(event_to_requeue, EventPriority::Normal);
}
self.manage_bot_population();
}
async fn process_client_broadcast(
peer_id_str: &str,
client_info: &ClientInfo,
shared_data: &SharedBroadcastData,
server: &Arc<MassiveGameServer>, ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let frame = server.frame_counter.load(AtomicOrdering::Relaxed);
trace!("[Frame {}] Starting broadcast for client {}", frame, peer_id_str);
let player_exists = {
let player_id_arc = server.player_manager.id_pool.get_or_create(peer_id_str);
server.player_manager.get_player_state(&player_id_arc).is_some()
};
if !player_exists {
warn!("[Frame {}] Player {} not found, skipping broadcast for this client.", frame, peer_id_str);
return Ok(());
}
let state_result = if client_info.needs_initial_state {
trace!("[Frame {}] Building initial state for {}", frame, peer_id_str);
server.build_initial_state_optimized(peer_id_str, shared_data).await
} else {
trace!("[Frame {}] Building delta state for {}", frame, peer_id_str);
let client_state_snapshot = server.client_states_map
.read() .get(peer_id_str)
.map(|cs_state_ref| cs_state_ref.clone()) .unwrap_or_else(|| {
warn!("[Frame {}] ClientState not found for {} during delta build, using default. This might indicate a logic issue.", server.frame_counter.load(AtomicOrdering::Relaxed), peer_id_str);
ClientState::default()
});
server.build_delta_state_optimized(peer_id_str, &client_state_snapshot, shared_data).await
};
let bytes_to_send = match state_result {
Ok(b) => {
trace!("[Frame {}] State built successfully for {} ({} bytes)", frame, peer_id_str, b.len());
b
}
Err(_e) => {
error!("[Frame {}] Failed to build state for {}: {:?}", frame, peer_id_str, _e);
return Err(format!("Failed to build state for client {}", peer_id_str).into());
}
};
trace!("[Frame {}] Sending {} bytes to client {}", frame, bytes_to_send.len(), peer_id_str);
const SEND_TIMEOUT_MS: u64 = 50;
match tokio::time::timeout(
Duration::from_millis(SEND_TIMEOUT_MS),
client_info.data_channel.send(&bytes_to_send)
).await {
Ok(Ok(_)) => {
trace!("[Frame {}] Data sent successfully to {}", frame, peer_id_str);
}
Ok(Err(e)) => {
warn!("[Frame {}] Send error for client {}: {:?}", frame, peer_id_str, e);
}
Err(_) => {
warn!("[Frame {}] Send timeout for client {} after {}ms", frame, peer_id_str, SEND_TIMEOUT_MS);
}
}
trace!("[Frame {}] Updating client state for {}", frame, peer_id_str);
if client_info.needs_initial_state {
server.update_client_state_after_initial(peer_id_str, shared_data);
} else {
let player_id = server.player_manager.id_pool.get_or_create(peer_id_str);
let mut client_states_guard = server.client_states_map.write();
if let Some(client_state) = client_states_guard.get_mut(peer_id_str) {
let mut client_state_clone = client_state.clone();
drop(client_states_guard);
server.update_client_state_after_delta(&mut client_state_clone, &player_id);
server.client_states_map.write().insert(peer_id_str.to_string(), client_state_clone);
}
}
if !client_info.needs_initial_state {
let client_state_option = server.client_states_map
.read() .get(peer_id_str)
.cloned();
if let Some(client_state_for_chat) = client_state_option {
server.send_chat_messages_optimized(
peer_id_str,
&client_info.data_channel,
&client_state_for_chat,
&shared_data.chat_messages
).await;
}
}
trace!("[Frame {}] Broadcast processing complete for client {}", frame, peer_id_str);
Ok(())
}
fn get_server_timestamp(&self) -> u64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64
}
fn process_melee_hits(&self, melee_hit_events: Vec<GameEvent>) {
for event in melee_hit_events {
if let GameEvent::MeleeHit { attacker_id, position: _attack_pos, .. } = event {
let melee_range_sq = 50.0 * 50.0;
let melee_arc_angle_rad = std::f32::consts::FRAC_PI_3;
let melee_damage = 30;
let (attacker_pos_x, attacker_pos_y, attacker_rot, attacker_team_id, attacker_username) = {
if let Some(attacker_state_guard) = self.player_manager.get_player_state(&attacker_id) {
(
attacker_state_guard.x,
attacker_state_guard.y,
attacker_state_guard.rotation,
attacker_state_guard.team_id,
attacker_state_guard.username.clone()
)
} else {
continue; }
};
let melee_check_radius = 70.0;
let nearby_player_ids = self.spatial_index.query_nearby_players(attacker_pos_x, attacker_pos_y, melee_check_radius);
for target_id_arc_nearby in nearby_player_ids {
if target_id_arc_nearby == attacker_id { continue; }
let target_hit_data = {
if let Some(mut target_state_entry) = self.player_manager.get_player_state_mut(&target_id_arc_nearby) {
let target_state = &mut *target_state_entry;
if !target_state.alive ||
(target_state.team_id != 0 && attacker_team_id != 0 && target_state.team_id == attacker_team_id) {
continue; }
let dx = target_state.x - attacker_pos_x;
let dy = target_state.y - attacker_pos_y;
let dist_sq = dx*dx + dy*dy;
if dist_sq >= melee_range_sq {
continue; }
let angle_to_target = dy.atan2(dx);
let mut angle_diff = (angle_to_target - attacker_rot).rem_euclid(2.0 * std::f32::consts::PI);
if angle_diff > std::f32::consts::PI {
angle_diff = 2.0 * std::f32::consts::PI - angle_diff;
}
if angle_diff > melee_arc_angle_rad / 2.0 {
continue; }
info!("[Melee] {} attempting to hit {} (dist_sq: {:.1}, angle_diff: {:.2} rad).",
attacker_id.as_str(), target_id_arc_nearby.as_str(), dist_sq, angle_diff);
let died = target_state.apply_damage(melee_damage);
let target_position = Vec2::new(target_state.x, target_state.y);
let target_username = target_state.username.clone();
let victim_was_carrying_flag_id = if died { target_state.is_carrying_flag_team_id } else { 0 };
if died {
target_state.is_carrying_flag_team_id = 0;
target_state.mark_field_changed(FIELD_FLAG);
}
Some((died, target_position, target_username, victim_was_carrying_flag_id))
} else {
None
}
};
if let Some((died, target_position, target_username, victim_was_carrying_flag_id)) = target_hit_data {
self.global_game_events.push(GameEvent::PlayerDamaged {
target_id: target_id_arc_nearby.clone(),
attacker_id: Some(attacker_id.clone()),
damage: melee_damage,
weapon: ServerWeaponType::Melee,
position: target_position,
}, EventPriority::Normal);
if died {
if attacker_id != target_id_arc_nearby {
if let Some(mut attacker_mut_state_entry) = self.player_manager.get_player_state_mut(&attacker_id) {
let attacker_mut_state = &mut *attacker_mut_state_entry;
attacker_mut_state.kills += 1;
attacker_mut_state.score += 100;
attacker_mut_state.mark_field_changed(FIELD_SCORE_STATS);
}
}
self.global_game_events.push(GameEvent::PlayerKilled {
victim_id: target_id_arc_nearby.clone(),
killer_id: attacker_id.clone(),
weapon: ServerWeaponType::Melee,
position: target_position,
}, EventPriority::High);
{
let mut kill_feed_guard = self.kill_feed.write();
kill_feed_guard.push_back(ServerKillFeedEntry {
killer_name: attacker_username.clone(),
victim_name: target_username,
weapon: ServerWeaponType::Melee,
timestamp: self.frame_counter.load(std::sync::atomic::Ordering::Relaxed),
});
if kill_feed_guard.len() > MAX_KILL_FEED_HISTORY {
kill_feed_guard.pop_front();
}
}
if victim_was_carrying_flag_id != 0 {
let mut match_info_guard = self.match_info.write();
if let Some(attacker_state_for_score) = self.player_manager.get_player_state(&attacker_id) {
if attacker_state_for_score.team_id != 0 &&
attacker_state_for_score.team_id != victim_was_carrying_flag_id {
let team_score_mut_ref = match_info_guard
.team_scores
.entry(attacker_state_for_score.team_id)
.or_insert(0);
*team_score_mut_ref += 1;
info!("Team {} scored +1 via melee kill on flag carrier by {}",
attacker_state_for_score.team_id, attacker_id.as_str());
}
}
if let Some(flag_state) = match_info_guard.flag_states.get_mut(&victim_was_carrying_flag_id) {
flag_state.status = fb::FlagStatus::Dropped;
flag_state.position = target_position;
flag_state.carrier_id = None;
flag_state.respawn_timer = 30.0;
drop(match_info_guard);
self.global_game_events.push(GameEvent::FlagDropped {
player_id: target_id_arc_nearby.clone(),
flag_team_id: victim_was_carrying_flag_id,
position: target_position
}, EventPriority::High);
info!("(Melee Kill) Flag of team {} dropped at ({:.1}, {:.1})",
victim_was_carrying_flag_id, target_position.x, target_position.y);
}
}
}
}
}
}
}
}
fn initialize_ctf_flags(&self, match_info: &mut ServerMatchInfo) {
match_info.flag_states.clear();
let team1_flag_pos = Self::get_flag_base_position(1);
match_info.flag_states.insert(1, ServerFlagState {
team_id: 1,
status: fb::FlagStatus::AtBase,
position: team1_flag_pos,
carrier_id: None,
respawn_timer: 0.0,
});
let team2_flag_pos = Self::get_flag_base_position(2);
match_info.flag_states.insert(2, ServerFlagState {
team_id: 2,
status: fb::FlagStatus::AtBase,
position: team2_flag_pos,
carrier_id: None,
respawn_timer: 0.0,
});
info!("CTF Flags initialized. T1 at {:?}, T2 at {:?}", team1_flag_pos, team2_flag_pos);
}
pub fn get_flag_base_position(team_id: u8) -> Vec2 {
if team_id == 1 {
Vec2::new(WORLD_MIN_X + 100.0, 0.0)
} else if team_id == 2 {
Vec2::new(WORLD_MAX_X - 100.0, 0.0)
} else {
Vec2::new(0.0, 0.0)
}
}
fn reset_match_state(&self, match_info: &mut ServerMatchInfo) {
match_info.time_remaining = 300.0;
match_info.team_scores.clear();
match_info.flag_states.clear();
if match_info.match_state == fb::MatchStateType::Waiting && match_info.game_mode == fb::GameModeType::CaptureTheFlag {
self.initialize_ctf_flags(match_info);
}
self.player_manager.for_each_player_mut(|_id, pstate| {
pstate.score = 0;
pstate.kills = 0;
pstate.deaths = 0;
pstate.is_carrying_flag_team_id = 0;
pstate.mark_field_changed(FIELD_SCORE_STATS | FIELD_FLAG);
});
self.kill_feed.write().clear();
}
async fn prepare_shared_broadcast_data(&self) -> SharedBroadcastData {
let current_timestamp_ms = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64;
let mut events = Vec::with_capacity(100);
while let Some(event) = self.global_game_events.pop() {
events.push(event);
if events.len() >= 100 { break; }
}
let destroyed_wall_ids = self.destroyed_wall_ids_this_tick
.read()
.iter()
.cloned()
.collect();
let chat_messages = self.chat_messages_queue
.read()
.await
.iter()
.cloned()
.collect();
let match_info_guard = self.match_info.read();
let match_info_snapshot = MatchInfoSnapshot {
time_remaining: match_info_guard.time_remaining,
match_state: match_info_guard.match_state,
game_mode: match_info_guard.game_mode,
team_scores: match_info_guard.team_scores.clone(),
flag_states: match_info_guard.flag_states.clone(),
};
drop(match_info_guard);
let kill_feed_snapshot = self.kill_feed
.read()
.iter()
.cloned()
.collect();
SharedBroadcastData {
timestamp_ms: current_timestamp_ms,
events,
destroyed_wall_ids,
chat_messages,
match_info_snapshot,
kill_feed_snapshot,
}
}
#[allow(dead_code)]
async fn process_client_broadcast_static(
peer_id_str: String,
data_channel: Arc<crate::core::types::RTCDataChannel>,
shared_data: &SharedBroadcastData,
player_manager: &Arc<ImprovedPlayerManager>,
player_aois: &PlayerAoIs,
client_states_map: &ClientStatesMap,
_world_partition_manager: &Arc<WorldPartitionManager>,
_pickups: &Arc<ParkingLotRwLock<Vec<Pickup>>>,
projectiles: &Arc<ParkingLotRwLock<Vec<Projectile>>>,
kill_feed: &Arc<ParkingLotRwLock<VecDeque<ServerKillFeedEntry>>>,
chat_messages_queue: &ChatMessagesQueue,
frame_num: u64,
) {
let mut client_state_copy = client_states_map
.read()
.get(&peer_id_str)
.cloned()
.unwrap_or_else(|| ClientState::default());
if !client_state_copy.known_walls_sent {
warn!("[Frame {}] Client {} needs initial state in process_client_broadcast_static. This path might be deprecated or an error.", frame_num, peer_id_str);
return;
}
let message_result = Self::build_delta_state_static(
&peer_id_str,
&client_state_copy,
shared_data,
player_manager,
player_aois,
_pickups,
projectiles,
kill_feed,
).await;
if let Ok(bytes) = message_result {
if let Err(e) = data_channel.send(&bytes).await {
handle_dc_send_error(&e.to_string(), &peer_id_str, "delta state (static path)");
}
}
Self::send_chat_messages_static(
&peer_id_str,
&data_channel,
&mut client_state_copy,
&shared_data.chat_messages,
chat_messages_queue,
).await;
Self::update_client_state_after_delta_static(
&peer_id_str,
client_state_copy,
shared_data,
client_states_map,
frame_num,
);
}
fn manage_bot_population(&self) { let human_player_count = self.player_manager.player_count().saturating_sub(self.bot_players.len());
let current_bot_count = self.bot_players.len();
let max_players_in_match = self.config.max_players_per_match;
let desired_bot_count = if human_player_count >= max_players_in_match {
0
} else {
(max_players_in_match - human_player_count).min(self.target_bot_count.load(std::sync::atomic::Ordering::Relaxed) as usize) };
if current_bot_count > desired_bot_count {
let bots_to_remove_count = current_bot_count - desired_bot_count;
debug!("[Bot Management] Max players: {}, Humans: {}, Current Bots: {}, Desired Bots: {}. Removing {} bots.",
max_players_in_match, human_player_count, current_bot_count, desired_bot_count, bots_to_remove_count);
self.remove_bots(bots_to_remove_count);
} else if current_bot_count < desired_bot_count {
let bots_to_add_count = desired_bot_count - current_bot_count;
debug!("[Bot Management] Max players: {}, Humans: {}, Current Bots: {}, Desired Bots: {}. Adding {} bots.",
max_players_in_match, human_player_count, current_bot_count, desired_bot_count, bots_to_add_count);
self.spawn_additional_bots(bots_to_add_count);
}
}
fn spawn_additional_bots(&self, count_to_add: usize) {
if count_to_add == 0 {
return;
}
info!("[Bot Management] Attempting to spawn {} additional bots...", count_to_add);
let team_spawn_areas = crate::world::map_generator::MapGenerator::get_team_spawn_areas();
let mut rng = rand::thread_rng();
let bot_names = ["Alpha", "Beta", "Gamma", "Delta", "Echo", "Foxtrot", "Golf", "Hotel", "India", "Juliet", "Kilo", "Lima", "Mike", "November", "Oscar", "Papa", "Quebec", "Romeo", "Sierra", "Tango", "Uniform", "Victor", "Whiskey", "Xray", "Yankee", "Zulu"];
for _i in 0..count_to_add { let current_total_players = self.player_manager.player_count();
if current_total_players >= self.config.max_players_per_match {
info!("[Bot Management] Max player limit ({}) reached, stopping additional bot spawn. Current players: {}", self.config.max_players_per_match, current_total_players);
break;
}
let bot_name_num = self.bot_name_counter.fetch_add(1, AtomicOrdering::SeqCst);
let bot_base_name = bot_names.get(bot_name_num as usize % bot_names.len()).unwrap_or(&"Extra");
let bot_name = format!("Bot {}{}", bot_base_name, if bot_name_num >= bot_names.len() as u64 { (bot_name_num / bot_names.len() as u64).to_string() } else { "".to_string() });
let bot_player_id_str = format!("bot_{}", uuid::Uuid::new_v4());
let mut team1_player_count = 0; let mut team2_player_count = 0; self.player_manager.for_each_player(|_id, p_state| {
if p_state.team_id == 1 { team1_player_count +=1; }
else if p_state.team_id == 2 { team2_player_count +=1; }
});
let team_id = if team1_player_count <= team2_player_count { 1 } else { 2 };
let bot_player_id_for_respawn = Arc::new(bot_player_id_str.clone());
let spawn_pos = {
let center_x = 0.0; let center_y = 0.0;
let spread_radius = 500.0; let angle = rng.gen_range(0.0..2.0 * std::f32::consts::PI);
let distance = rng.gen_range(0.0..spread_radius);
Vec2::new(
center_x + distance * angle.cos(),
center_y + distance * angle.sin()
)
};
if let Some(player_id_arc) = self.player_manager.add_player(bot_player_id_str.clone(), bot_name.clone(), spawn_pos.x, spawn_pos.y) {
if let Some(mut p_state_entry) = self.player_manager.get_player_state_mut(&player_id_arc) {
let p_state = &mut *p_state_entry;
p_state.team_id = team_id;
p_state.mark_field_changed(FIELD_SCORE_STATS | FIELD_FLAG);
}
let bot_controller = BotController {
player_id: player_id_arc.clone(),
target_position: None,
target_enemy_id: None,
last_decision_time: Instant::now(),
behavior_state: BotBehaviorState::Idle,
current_path: VecDeque::new(),
path_recalculation_timer: Instant::now(),
};
self.bot_players.insert(player_id_arc, bot_controller);
debug!("[Bot Management] Spawned additional bot: {} (ID: {}) on team {} at ({:.1}, {:.1}). Total players: {}", bot_name, bot_player_id_str, team_id, spawn_pos.x, spawn_pos.y, self.player_manager.player_count());
} else {
error!("[Bot Management] Failed to add bot {} to player manager.", bot_name);
}
}
}
fn remove_bots(&self, count: usize) {
let mut removed_count = 0;
let bot_keys_to_remove: Vec<PlayerID> = self.bot_players.iter()
.map(|entry| entry.key().clone())
.take(count)
.collect();
for bot_key in bot_keys_to_remove {
if self.bot_players.remove(&bot_key).is_some() {
self.player_manager.remove_player(bot_key.as_str());
info!("[Bot Management] Removed bot {} to adjust match population.", bot_key);
removed_count += 1;
if removed_count >= count {
break;
}
}
}
}
pub async fn build_delta_state_optimized(
&self,
peer_id_str: &str,
client_state: &ClientState,
shared_data: &SharedBroadcastData,
) -> Result<Bytes, Box<dyn std::error::Error + Send + Sync>> {
use std::cell::RefCell;
thread_local! {
static BUILDER: RefCell<flatbuffers::FlatBufferBuilder<'static>> =
RefCell::new(flatbuffers::FlatBufferBuilder::with_capacity(16384));
}
BUILDER.with(|builder_cell| {
let mut builder = builder_cell.borrow_mut();
builder.reset();
let build_start = Instant::now();
let player_id = self.player_manager.id_pool.get_or_create(peer_id_str);
trace!("[{}] DeltaBuilder: Started", peer_id_str);
let player_aoi = self.player_aois
.get(peer_id_str)
.map(|entry| entry.value().clone())
.unwrap_or_else(|| PlayerAoI::new());
let mut players_fb_vec = Vec::new();
let mut removed_player_ids_vec = Vec::new();
if let Some(self_state) = self.player_manager.get_player_state(&player_id) {
players_fb_vec.push(create_fb_player_state_for_delta(&mut builder, &self_state, 0xFFFF));
}
for visible_player_id in &player_aoi.visible_players {
if visible_player_id != &player_id {
if let Some(player_state) = self.player_manager.get_player_state(visible_player_id) {
players_fb_vec.push(create_fb_player_state_for_delta(&mut builder, &player_state, 0xFFFF));
}
}
}
for known_player_id in &client_state.last_known_players {
if !player_aoi.visible_players.contains(known_player_id) && known_player_id != &player_id {
removed_player_ids_vec.push(builder.create_string(known_player_id.as_str()));
}
}
let players_fb = builder.create_vector(&players_fb_vec);
let removed_players_fb = builder.create_vector(&removed_player_ids_vec);
let mut new_projectiles_vec = Vec::new();
let mut removed_projectile_ids_vec = Vec::new();
let projectiles_guard = self.projectiles.read();
for proj_id in &player_aoi.visible_projectiles {
if !client_state.last_known_projectile_ids.contains(proj_id) {
if let Some(proj) = projectiles_guard.iter().find(|p| p.id == *proj_id) {
let id_str = builder.create_string(&proj.id.to_string());
let owner_str = builder.create_string(proj.owner_id.as_str());
let proj_fb = fb::ProjectileState::create(&mut builder, &fb::ProjectileStateArgs {
id: Some(id_str),
x: proj.x,
y: proj.y,
owner_id: Some(owner_str),
weapon_type: map_server_weapon_to_fb(proj.weapon_type),
velocity_x: proj.velocity_x, velocity_y: proj.velocity_y, });
new_projectiles_vec.push(proj_fb);
}
}
}
for known_proj_id in &client_state.last_known_projectile_ids {
if !player_aoi.visible_projectiles.contains(known_proj_id) {
let id_str = builder.create_string(&known_proj_id.to_string());
removed_projectile_ids_vec.push(id_str);
}
}
drop(projectiles_guard);
let projectiles_fb = builder.create_vector(&new_projectiles_vec);
let removed_projectiles_fb = builder.create_vector(&removed_projectile_ids_vec);
let mut pickups_delta_vec = Vec::new();
let mut deactivated_pickup_ids_vec = Vec::new();
let pickups_guard = self.pickups.read();
for pickup_id in &player_aoi.visible_pickups {
if let Some(pickup) = pickups_guard.iter().find(|p| p.id == *pickup_id) {
let should_send = if let Some(last_known_state) = client_state.last_known_pickup_states.get(pickup_id) {
last_known_state.is_active != pickup.is_active
} else {
true
};
if should_send {
let (pickup_type_fb, weapon_type_fb) = map_core_pickup_to_fb(&pickup.pickup_type);
let id_str = builder.create_string(&pickup.id.to_string());
let pickup_fb = fb::Pickup::create(&mut builder, &fb::PickupArgs {
id: Some(id_str),
x: pickup.x,
y: pickup.y,
pickup_type: pickup_type_fb,
weapon_type: weapon_type_fb.unwrap_or(fb::WeaponType::Pistol),
is_active: pickup.is_active,
});
pickups_delta_vec.push(pickup_fb);
}
}
}
for (known_pickup_id, _) in &client_state.last_known_pickup_states {
if !player_aoi.visible_pickups.contains(known_pickup_id) {
let id_str = builder.create_string(&known_pickup_id.to_string());
deactivated_pickup_ids_vec.push(id_str);
}
}
drop(pickups_guard);
let pickups_fb = builder.create_vector(&pickups_delta_vec);
let deactivated_pickups_fb = builder.create_vector(&deactivated_pickup_ids_vec);
let events_vec: Vec<_> = shared_data.events.iter().take(50).map(|event| {
build_game_event_fb(&mut builder, event)
}).collect();
let game_events_fb = builder.create_vector(&events_vec);
let kill_feed_vec: Vec<_> = shared_data.kill_feed_snapshot.iter().map(|entry| {
let killer_name_fb = builder.create_string(&entry.killer_name);
let victim_name_fb = builder.create_string(&entry.victim_name);
fb::KillFeedEntry::create(&mut builder, &fb::KillFeedEntryArgs {
killer_name: Some(killer_name_fb),
victim_name: Some(victim_name_fb),
weapon: map_server_weapon_to_fb(entry.weapon),
timestamp: entry.timestamp as f32,
killer_position: None,
victim_position: None,
is_headshot: false,
})
}).collect();
let kill_feed_fb = builder.create_vector(&kill_feed_vec);
let match_info_fb = {
let match_snapshot = &shared_data.match_info_snapshot;
let team_scores_vec: Vec<_> = match_snapshot.team_scores.iter().map(|(team_id, score)| {
fb::TeamScoreEntry::create(&mut builder, &fb::TeamScoreEntryArgs {
team_id: *team_id as i8,
score: *score,
})
}).collect();
let team_scores_fb = builder.create_vector(&team_scores_vec);
Some(fb::MatchInfo::create(&mut builder, &fb::MatchInfoArgs {
time_remaining: match_snapshot.time_remaining,
match_state: match_snapshot.match_state,
winner_id: None,
winner_name: None,
game_mode: match_snapshot.game_mode,
team_scores: Some(team_scores_fb),
}))
};
let destroyed_walls_vec: Vec<_> = shared_data.destroyed_wall_ids.iter()
.map(|id| builder.create_string(&id.to_string()))
.collect();
let destroyed_wall_ids_fb = if !destroyed_walls_vec.is_empty() {
Some(builder.create_vector(&destroyed_walls_vec))
} else {
None
};
let delta_state_args = fb::DeltaStateMessageArgs {
players: Some(players_fb),
projectiles: Some(projectiles_fb),
removed_projectiles: Some(removed_projectiles_fb),
pickups: Some(pickups_fb),
deactivated_pickup_ids: Some(deactivated_pickups_fb),
game_events: Some(game_events_fb),
timestamp: shared_data.timestamp_ms,
last_processed_input_sequence: 0, changed_player_fields: None,
kill_feed: Some(kill_feed_fb),
match_info: match_info_fb,
destroyed_wall_ids: destroyed_wall_ids_fb,
flag_states: None,
removed_player_ids: Some(removed_players_fb),
updated_walls: None,
};
let delta_state = fb::DeltaStateMessage::create(&mut builder, &delta_state_args);
let game_msg = fb::GameMessage::create(&mut builder, &fb::GameMessageArgs {
msg_type: fb::MessageType::DeltaState,
actual_message_type: fb::MessagePayload::DeltaStateMessage,
actual_message: Some(delta_state.as_union_value()),
});
builder.finish(game_msg, None);
let bytes = Bytes::from(builder.finished_data().to_vec());
trace!("[{}] DeltaBuilder: Completed in {:?}", peer_id_str, build_start.elapsed());
Ok(bytes)
})
}
fn build_projectile_deltas_optimized<'a>(
&self,
builder: &mut flatbuffers::FlatBufferBuilder<'a>,
aoi_data: &PlayerAoI,
client_state: &ClientState,
) -> (
flatbuffers::WIPOffset<flatbuffers::Vector<'a, flatbuffers::ForwardsUOffset<fb::ProjectileState<'a>>>>,
flatbuffers::WIPOffset<flatbuffers::Vector<'a, flatbuffers::ForwardsUOffset<&'a str>>>
) {
let mut new_projectiles_fb_vec = Vec::new();
let mut removed_projectiles_ids_str_vec = Vec::new();
let projectiles_read_guard = self.projectiles.read();
let current_projectiles_in_aoi: HashSet<EntityId> = aoi_data.visible_projectiles.clone();
for proj_id in current_projectiles_in_aoi.iter() {
if !client_state.last_known_projectile_ids.contains(proj_id) {
if let Some(proj) = projectiles_read_guard.iter().find(|p| p.id == *proj_id) {
let id_fb = builder.create_string(&proj.id.to_string());
let owner_id_fb = builder.create_string(proj.owner_id.as_str());
new_projectiles_fb_vec.push(fb::ProjectileState::create(builder, &fb::ProjectileStateArgs{
id: Some(id_fb),
x: proj.x,
y: proj.y,
owner_id: Some(owner_id_fb),
weapon_type: map_server_weapon_to_fb(proj.weapon_type),
velocity_x: proj.velocity_x,
velocity_y: proj.velocity_y,
}));
}
}
}
for known_proj_id in client_state.last_known_projectile_ids.iter() {
if !current_projectiles_in_aoi.contains(known_proj_id) {
removed_projectiles_ids_str_vec.push(known_proj_id.to_string());
}
}
let projectiles_fb = builder.create_vector(&new_projectiles_fb_vec);
let removed_projectiles_fb_offsets: Vec<_> = removed_projectiles_ids_str_vec.iter()
.map(|s| builder.create_string(s))
.collect();
let removed_projectiles_fb = builder.create_vector(&removed_projectiles_fb_offsets);
(projectiles_fb, removed_projectiles_fb)
}
fn build_events_fb<'a>(
&self,
builder: &mut flatbuffers::FlatBufferBuilder<'a>,
events: &[GameEvent],
) -> flatbuffers::WIPOffset<flatbuffers::Vector<'a, flatbuffers::ForwardsUOffset<fb::GameEvent<'a>>>> {
let game_events_fb_vec: Vec<_> = events.iter().take(50).map(|event| {
let event_pos = event_position(event);
let pos_fb_offset = fb::Vec2::create(builder, &fb::Vec2Args { x: event_pos.x, y: event_pos.y });
let instigator_id_fb = event_instigator_id(event).map(|id| builder.create_string(id.as_str()));
let target_id_fb = event_target_id(event).map(|id_str| builder.create_string(&id_str));
let weapon_type_fb = event_weapon_type(event).map_or(fb::WeaponType::Pistol, map_server_weapon_to_fb);
fb::GameEvent::create(builder, &fb::GameEventArgs{
event_type: map_game_event_type_to_fb(event),
position: Some(pos_fb_offset),
instigator_id: instigator_id_fb,
target_id: target_id_fb,
weapon_type: weapon_type_fb,
value: event_value(event).unwrap_or(0.0),
})
}).collect();
builder.create_vector(&game_events_fb_vec)
}
async fn build_delta_state_static(
peer_id_str: &str,
client_state: &ClientState,
shared_data: &SharedBroadcastData,
player_manager: &Arc<ImprovedPlayerManager>,
player_aois: &PlayerAoIs,
_pickups: &Arc<ParkingLotRwLock<Vec<Pickup>>>,
_projectiles: &Arc<ParkingLotRwLock<Vec<Projectile>>>,
_kill_feed: &Arc<ParkingLotRwLock<VecDeque<ServerKillFeedEntry>>>,
) -> Result<Bytes, ()> {
thread_local! {
static BUILDER: RefCell<flatbuffers::FlatBufferBuilder<'static>> =
RefCell::new(flatbuffers::FlatBufferBuilder::with_capacity(16384));
}
BUILDER.with(|builder_cell| {
let mut builder = builder_cell.borrow_mut();
builder.reset();
let self_player_id = player_manager.id_pool.get_or_create(peer_id_str);
let mut last_processed_input_for_client = 0;
let mut players_delta_fb_vec = Vec::new();
let mut player_fields_mask_fb_vec = Vec::new();
if let Some(self_player_state_guard) = player_manager.get_player_state(&self_player_id) {
let self_player_state = &*self_player_state_guard;
last_processed_input_for_client = self_player_state.last_processed_input_sequence;
if self_player_state.changed_fields > 0 {
players_delta_fb_vec.push(create_fb_player_state_for_delta(&mut builder, self_player_state, self_player_state.changed_fields));
player_fields_mask_fb_vec.push(self_player_state.changed_fields as u8);
}
}
if let Some(aoi_entry) = player_aois.get(peer_id_str) {
let p_aoi = aoi_entry.value();
for visible_player_id in p_aoi.visible_players.iter() {
if visible_player_id == &self_player_id { continue; }
if let Some(other_pstate_guard) = player_manager.get_player_state(visible_player_id) {
let other_pstate = &*other_pstate_guard;
if other_pstate.changed_fields > 0 ||
client_state.last_known_player_states.get(visible_player_id).map_or(true, |old_ps| *old_ps != *other_pstate) {
players_delta_fb_vec.push(create_fb_player_state_for_delta(&mut builder, other_pstate, other_pstate.changed_fields));
player_fields_mask_fb_vec.push(other_pstate.changed_fields as u8);
}
}
}
}
let players_fb = builder.create_vector(&players_delta_fb_vec);
let player_fields_mask_fb = builder.create_vector(&player_fields_mask_fb_vec);
let empty_projectiles: Vec<flatbuffers::WIPOffset<fb::ProjectileState>> = Vec::new();
let projectiles_fb = builder.create_vector(&empty_projectiles);
let empty_strings: Vec<flatbuffers::WIPOffset<&str>> = Vec::new();
let removed_projectiles_fb = builder.create_vector(&empty_strings);
let empty_pickups: Vec<flatbuffers::WIPOffset<fb::Pickup>> = Vec::new();
let pickups_delta_fb = builder.create_vector(&empty_pickups);
let deactivated_pickups_fb = builder.create_vector(&empty_strings);
let game_events_fb_vec: Vec<_> = shared_data.events.iter().take(50).map(|event| {
let event_pos = event_position(event);
let pos_fb_offset = fb::Vec2::create(&mut builder, &fb::Vec2Args { x: event_pos.x, y: event_pos.y });
let instigator_id_fb = event_instigator_id(event).map(|id| builder.create_string(id.as_str()));
let target_id_fb = event_target_id(event).map(|id_str| builder.create_string(&id_str));
let weapon_type_fb = event_weapon_type(event).map_or(fb::WeaponType::Pistol, map_server_weapon_to_fb);
fb::GameEvent::create(&mut builder, &fb::GameEventArgs{
event_type: map_game_event_type_to_fb(event),
position: Some(pos_fb_offset),
instigator_id: instigator_id_fb,
target_id: target_id_fb,
weapon_type: weapon_type_fb,
value: event_value(event).unwrap_or(0.0),
})
}).collect();
let game_events_fb = builder.create_vector(&game_events_fb_vec);
let destroyed_walls_fb_vec: Vec<_> = shared_data.destroyed_wall_ids
.iter()
.take(20)
.map(|wall_id| builder.create_string(&wall_id.to_string()))
.collect();
let destroyed_walls_fb = if !destroyed_walls_fb_vec.is_empty() {
Some(builder.create_vector(&destroyed_walls_fb_vec))
} else {
None
};
let delta_state_args = fb::DeltaStateMessageArgs {
players: Some(players_fb),
projectiles: Some(projectiles_fb),
removed_projectiles: Some(removed_projectiles_fb),
pickups: Some(pickups_delta_fb),
deactivated_pickup_ids: Some(deactivated_pickups_fb),
game_events: Some(game_events_fb),
timestamp: shared_data.timestamp_ms,
last_processed_input_sequence: last_processed_input_for_client,
changed_player_fields: Some(player_fields_mask_fb),
kill_feed: None,
match_info: None,
destroyed_wall_ids: destroyed_walls_fb,
flag_states: None,
removed_player_ids: None,
updated_walls: None,
};
let delta_state_msg = fb::DeltaStateMessage::create(&mut builder, &delta_state_args);
let game_msg = fb::GameMessage::create(&mut builder, &fb::GameMessageArgs {
msg_type: fb::MessageType::DeltaState,
actual_message_type: fb::MessagePayload::DeltaStateMessage,
actual_message: Some(delta_state_msg.as_union_value()),
});
builder.finish(game_msg, None);
Ok(Bytes::from(builder.finished_data().to_vec()))
})
}
fn get_player_aoi_data_fast(&self, player_id: &PlayerID) -> PlayerAoI {
if let Some(aoi_entry) = self.player_aois.get(player_id.as_str()) {
PlayerAoI {
visible_players: aoi_entry.visible_players.clone(),
visible_projectiles: aoi_entry.visible_projectiles.clone(),
visible_pickups: aoi_entry.visible_pickups.clone(),
visible_walls: aoi_entry.visible_walls.clone(),
last_update: aoi_entry.last_update.clone(), }
} else {
Self::get_empty_player_aoi()
}
}
fn build_wall_deltas_optimized<'a>(
&self,
builder: &mut flatbuffers::FlatBufferBuilder<'a>,
player_aoi: &PlayerAoI,
last_known_walls: &HashSet<EntityId>,
) -> (
Vec<flatbuffers::WIPOffset<fb::Wall<'a>>>,
Vec<flatbuffers::WIPOffset<&'a str>>
) {
let mut new_or_changed_walls = Vec::new();
let mut destroyed_wall_ids = Vec::new();
for wall_id in &player_aoi.visible_walls {
let should_send_wall = !last_known_walls.contains(wall_id);
if should_send_wall {
for partition in self.world_partition_manager.get_partitions_for_processing() {
if let Some(wall) = partition.get_wall(*wall_id) {
if wall.is_destructible && wall.current_health <= 0 {
let id_str = builder.create_string(&wall_id.to_string());
destroyed_wall_ids.push(id_str);
} else {
let id_str = builder.create_string(&wall.id.to_string());
let wall_fb = fb::Wall::create(builder, &fb::WallArgs {
id: Some(id_str),
x: wall.x,
y: wall.y,
width: wall.width,
height: wall.height,
is_destructible: wall.is_destructible,
current_health: wall.current_health,
max_health: wall.max_health,
});
new_or_changed_walls.push(wall_fb);
}
break;
}
}
}
}
for known_wall_id in last_known_walls {
if !player_aoi.visible_walls.contains(known_wall_id) {
let id_str = builder.create_string(&known_wall_id.to_string());
destroyed_wall_ids.push(id_str);
}
}
(new_or_changed_walls, destroyed_wall_ids)
}
fn build_player_deltas_optimized<'a>(
&self,
builder: &mut flatbuffers::FlatBufferBuilder<'a>,
self_player_id: &PlayerID,
aoi_data: &PlayerAoI,
client_state: &ClientState,
) -> flatbuffers::WIPOffset<flatbuffers::Vector<'a, flatbuffers::ForwardsUOffset<fb::PlayerState<'a>>>> {
let mut players_fb_vec = Vec::new();
if let Some(self_state) = self.player_manager.get_player_state(self_player_id) {
if self_state.changed_fields > 0 {
players_fb_vec.push(create_fb_player_state_for_delta(builder, &self_state, self_state.changed_fields));
}
}
let visible_states: Vec<_> = aoi_data.visible_players
.iter()
.filter_map(|id| self.player_manager.get_player_state(id).map(|s| (id, s)))
.collect();
for (_id, state) in visible_states {
if state.changed_fields > 0 ||
!client_state.last_known_player_states.contains_key(_id) {
players_fb_vec.push(create_fb_player_state_for_delta(builder, &state, state.changed_fields));
}
}
builder.create_vector(&players_fb_vec)
}
fn create_fb_player_state_cached<'a>(
&self,
builder: &mut flatbuffers::FlatBufferBuilder<'a>,
state: &PlayerState,
) -> flatbuffers::WIPOffset<fb::PlayerState<'a>> {
create_fb_player_state_for_delta(builder, state, state.changed_fields)
}
pub async fn broadcast_world_updates_optimized(self: Arc<Self>) {
const BROADCAST_INTERVAL_FRAMES: u64 = 1;
let current_frame = self.frame_counter.load(AtomicOrdering::Relaxed);
let last_broadcast = self.last_broadcast_frame.load(AtomicOrdering::Relaxed);
if current_frame < last_broadcast + BROADCAST_INTERVAL_FRAMES && current_frame != 0 {
trace!("[Frame {}] Skipping broadcast (interval). Last broadcast: {}", current_frame, last_broadcast);
return;
}
let connected_clients = self.data_channels_map.len();
if connected_clients == 0 {
trace!("[Frame {}] Skipping broadcast (no connected clients).", current_frame);
return;
}
debug!("[Frame {}] Starting broadcast to {} clients. Last broadcast frame: {}", current_frame, connected_clients, last_broadcast);
self.last_broadcast_frame.store(current_frame, AtomicOrdering::Relaxed);
let shared_broadcast_data = self.prepare_shared_broadcast_data().await;
trace!("[Frame {}] Prepared shared broadcast data. Events: {}, Destroyed Walls: {}, Chat: {}, KF: {}",
current_frame, shared_broadcast_data.events.len(), shared_broadcast_data.destroyed_wall_ids.len(),
shared_broadcast_data.chat_messages.len(), shared_broadcast_data.kill_feed_snapshot.len());
let client_entries: Vec<_> = self.data_channels_map.iter()
.map(|entry| (entry.key().clone(), Arc::clone(entry.value())))
.collect();
for (peer_id_str, data_channel_arc) in client_entries {
let needs_initial = !self.client_states_map
.read() .get(&peer_id_str)
.map_or(false, |cs_state| cs_state.known_walls_sent);
let client_info = ClientInfo {
data_channel: data_channel_arc.clone(),
needs_initial_state: needs_initial,
};
trace!("[Frame {}] Processing client: {}, Needs Initial: {}", current_frame, peer_id_str, client_info.needs_initial_state);
if let Err(e) = Self::process_client_broadcast(&peer_id_str, &client_info, &shared_broadcast_data, &self).await {
error!("[Frame {}] Error processing broadcast for client {}: {:?}", current_frame, peer_id_str, e);
}
}
debug!("[Frame {}] Broadcast processing loop complete.", current_frame);
}
async fn build_initial_state_optimized(
&self,
peer_id_str: &str,
shared_data: &SharedBroadcastData, ) -> Result<Bytes, Box<dyn std::error::Error + Send + Sync>> {
const MAX_INITIAL_PLAYERS: usize = 50;
const MAX_INITIAL_WALLS: usize = 250; const MAX_INITIAL_PROJECTILES: usize = 50;
const MAX_INITIAL_PICKUPS: usize = 50;
const MAX_INITIAL_EVENTS: usize = 30;
const MAX_INITIAL_KILL_FEED: usize = 10;
const MAX_MESSAGE_SIZE_BYTES: usize = 60000;
thread_local! {
static BUILDER: RefCell<flatbuffers::FlatBufferBuilder<'static>> =
RefCell::new(flatbuffers::FlatBufferBuilder::with_capacity(32768)); }
BUILDER.with(|builder_cell| {
let mut builder = builder_cell.borrow_mut();
builder.reset();
let frame = self.frame_counter.load(AtomicOrdering::Relaxed);
info!("[Frame {}] Client {}: Building InitialStateMessage.", frame, peer_id_str);
let self_player_id_arc = self.player_manager.id_pool.get_or_create(peer_id_str);
let all_structural_walls: Vec<Wall> = match CACHED_WALLS.get() {
Some(cache_entry_arc) => {
let guard = cache_entry_arc.read();
info!("[Frame {} Client {}] InitialState: Getting all {} structural walls from cache (cache frame {}).",
frame, peer_id_str, guard.1.len(), guard.0);
guard.1.clone()
}
None => {
error!("[Frame {} Client {}] InitialState: CRITICAL - Wall cache not initialized! Sending empty wall list.", frame, peer_id_str);
Vec::new()
}
};
let mut walls_fb_vec = Vec::with_capacity(all_structural_walls.len().min(MAX_INITIAL_WALLS));
for wall_data in all_structural_walls.iter().take(MAX_INITIAL_WALLS) {
let id_fb = fb_safe_str(&mut builder, &wall_data.id.to_string());
walls_fb_vec.push(fb::Wall::create(&mut builder, &fb::WallArgs{
id: Some(id_fb), x: wall_data.x, y: wall_data.y, width: wall_data.width, height: wall_data.height,
is_destructible: wall_data.is_destructible,
current_health: wall_data.current_health,
max_health: wall_data.max_health,
}));
}
let walls_fb = builder.create_vector(&walls_fb_vec);
info!("[Frame {} Client {}] InitialState: Serialized {} walls.", frame, peer_id_str, walls_fb_vec.len());
let mut players_fb_vec = Vec::new();
let mut player_aoi_data_for_initial_state = Self::get_empty_player_aoi();
if let Some(self_pstate_guard) = self.player_manager.get_player_state(&self_player_id_arc) {
players_fb_vec.push(create_fb_player_state_for_delta(&mut builder, &*self_pstate_guard, 0xFFFF));
player_aoi_data_for_initial_state = self.get_player_aoi_data_fast(&self_player_id_arc);
} else {
warn!("[Frame {} Client {}] InitialState: Self player state not found!", frame, peer_id_str);
}
for visible_player_id in player_aoi_data_for_initial_state.visible_players.iter().take(MAX_INITIAL_PLAYERS.saturating_sub(players_fb_vec.len())) {
if visible_player_id != &self_player_id_arc { if let Some(pstate_guard) = self.player_manager.get_player_state(visible_player_id) {
players_fb_vec.push(create_fb_player_state_for_delta(&mut builder, &*pstate_guard, 0xFFFF));
}
}
}
let players_fb = builder.create_vector(&players_fb_vec);
info!("[Frame {} Client {}] InitialState: Serialized {} player states.", frame, peer_id_str, players_fb_vec.len());
let mut projectiles_fb_vec = Vec::new();
let projectiles_guard = self.projectiles.read();
for proj_id in player_aoi_data_for_initial_state.visible_projectiles.iter().take(MAX_INITIAL_PROJECTILES) {
if let Some(proj) = projectiles_guard.iter().find(|p| p.id == *proj_id) {
let id_fb = fb_safe_str(&mut builder, &proj.id.to_string());
let owner_id_fb = fb_safe_str(&mut builder, proj.owner_id.as_str());
projectiles_fb_vec.push(fb::ProjectileState::create(&mut builder, &fb::ProjectileStateArgs{
id: Some(id_fb), x: proj.x, y: proj.y, owner_id: Some(owner_id_fb),
weapon_type: map_server_weapon_to_fb(proj.weapon_type),
velocity_x: proj.velocity_x, velocity_y: proj.velocity_y,
}));
}
}
let projectiles_fb = builder.create_vector(&projectiles_fb_vec);
info!("[Frame {} Client {}] InitialState: Serialized {} projectiles.", frame, peer_id_str, projectiles_fb_vec.len());
let mut pickups_fb_vec = Vec::new();
let pickups_guard = self.pickups.read();
for pickup_id in player_aoi_data_for_initial_state.visible_pickups.iter().take(MAX_INITIAL_PICKUPS) {
if let Some(pickup) = pickups_guard.iter().find(|p| p.id == *pickup_id) {
if pickup.is_active { let (fb_pickup_type, fb_weapon_type_opt) = map_core_pickup_to_fb(&pickup.pickup_type);
let id_fb = fb_safe_str(&mut builder, &pickup.id.to_string());
pickups_fb_vec.push(fb::Pickup::create(&mut builder, &fb::PickupArgs {
id: Some(id_fb), x: pickup.x, y: pickup.y, pickup_type: fb_pickup_type,
weapon_type: fb_weapon_type_opt.unwrap_or(fb::WeaponType::Pistol),
is_active: pickup.is_active,
}));
}
}
}
let pickups_fb = builder.create_vector(&pickups_fb_vec);
info!("[Frame {} Client {}] InitialState: Serialized {} active pickups.", frame, peer_id_str, pickups_fb_vec.len());
let match_snapshot = &shared_data.match_info_snapshot;
let fb_team_scores_vec: Vec<_> = match_snapshot.team_scores.iter().map(|(team_id, score)| {
fb::TeamScoreEntry::create(&mut builder, &fb::TeamScoreEntryArgs { team_id: *team_id as i8, score: *score })
}).collect();
let team_scores_fb = builder.create_vector(&fb_team_scores_vec);
let match_info_fb = fb::MatchInfo::create(&mut builder, &fb::MatchInfoArgs {
time_remaining: match_snapshot.time_remaining,
match_state: match_snapshot.match_state,
winner_id: None, winner_name: None,
game_mode: match_snapshot.game_mode,
team_scores: Some(team_scores_fb),
});
let fb_flag_states_vec: Vec<_> = match_snapshot.flag_states.values().map(|fs| {
let carrier_id_fb = fs.carrier_id.as_ref().map(|id| fb_safe_str(&mut builder, id.as_str()));
let pos_fb = fb::Vec2::create(&mut builder, &fb::Vec2Args{ x: fs.position.x, y: fs.position.y });
fb::FlagState::create(&mut builder, &fb::FlagStateArgs {
team_id: fs.team_id as i8, status: fs.status, position: Some(pos_fb),
carrier_id: carrier_id_fb, respawn_timer: fs.respawn_timer,
})
}).collect();
let flag_states_fb = builder.create_vector(&fb_flag_states_vec);
let map_name_fb = fb_safe_str(&mut builder, "Massive Arena");
let timestamp_initial = shared_data.timestamp_ms;
let player_id_fb_initial = fb_safe_str(&mut builder, peer_id_str);
let initial_state_args = fb::InitialStateMessageArgs {
player_id: Some(player_id_fb_initial),
walls: Some(walls_fb),
players: Some(players_fb),
projectiles: Some(projectiles_fb),
pickups: Some(pickups_fb),
match_info: Some(match_info_fb),
flag_states: Some(flag_states_fb),
timestamp: timestamp_initial,
map_name: Some(map_name_fb),
};
let initial_state_msg = fb::InitialStateMessage::create(&mut builder, &initial_state_args);
let game_msg_args = fb::GameMessageArgs {
msg_type: fb::MessageType::InitialState,
actual_message_type: fb::MessagePayload::InitialStateMessage,
actual_message: Some(initial_state_msg.as_union_value()),
};
let game_msg = fb::GameMessage::create(&mut builder, &game_msg_args);
builder.finish(game_msg, None);
let finished_data = builder.finished_data();
info!("[Frame {} Client {}] InitialStateMessage built. Size: {} bytes.", frame, peer_id_str, finished_data.len());
if finished_data.len() > MAX_MESSAGE_SIZE_BYTES {
return Err("Initial state too large".into());
}
Ok(Bytes::from(finished_data.to_vec()))
})
}
async fn send_initial_state_to_client(&self, peer_id_str: &str, data_channel: &Arc<crate::core::types::RTCDataChannel>, client_state: &mut ClientState) {
info!("[{}] Sending initial state to client", peer_id_str);
let mut builder = flatbuffers::FlatBufferBuilder::with_capacity(16384);
let self_player_id_arc = self.player_manager.id_pool.get_or_create(peer_id_str);
let mut players_fb_vec = Vec::new();
if let Some(aoi_entry) = self.player_aois.get(peer_id_str) {
let p_aoi = aoi_entry.value();
if let Some(self_pstate_guard) = self.player_manager.get_player_state(&self_player_id_arc) {
players_fb_vec.push(create_fb_player_state_for_delta(&mut builder, &*self_pstate_guard, 0xFFFF));
}
for visible_player_id in p_aoi.visible_players.iter() {
if visible_player_id != &self_player_id_arc {
if let Some(pstate_guard) = self.player_manager.get_player_state(visible_player_id) {
players_fb_vec.push(create_fb_player_state_for_delta(&mut builder, &*pstate_guard, 0xFFFF));
}
}
}
} else {
if let Some(self_pstate_guard) = self.player_manager.get_player_state(&self_player_id_arc) {
players_fb_vec.push(create_fb_player_state_for_delta(&mut builder, &*self_pstate_guard, 0xFFFF));
}
}
let players_fb = builder.create_vector(&players_fb_vec);
let mut walls_fb_vec = Vec::new();
let current_active_walls = self.collect_all_walls_current_state();
for wall_data in current_active_walls {
let id_fb = builder.create_string(&wall_data.id.to_string());
walls_fb_vec.push(fb::Wall::create(&mut builder, &fb::WallArgs{
id: Some(id_fb), x: wall_data.x, y: wall_data.y, width: wall_data.width, height: wall_data.height,
is_destructible: wall_data.is_destructible, current_health: wall_data.current_health, max_health: wall_data.max_health,
}));
}
let walls_fb = builder.create_vector(&walls_fb_vec);
let mut projectiles_fb_vec = Vec::new();
if let Some(aoi_entry) = self.player_aois.get(peer_id_str) {
let p_aoi = aoi_entry.value();
let projectiles_guard = self.projectiles.read();
for proj_id in p_aoi.visible_projectiles.iter() {
if let Some(proj) = projectiles_guard.iter().find(|p| p.id == *proj_id) {
let id_fb = builder.create_string(&proj.id.to_string());
let owner_id_fb = builder.create_string(proj.owner_id.as_str());
projectiles_fb_vec.push(fb::ProjectileState::create(&mut builder, &fb::ProjectileStateArgs{
id: Some(id_fb), x: proj.x, y: proj.y, owner_id: Some(owner_id_fb),
weapon_type: map_server_weapon_to_fb(proj.weapon_type),
velocity_x: proj.velocity_x, velocity_y: proj.velocity_y,
}));
}
}
}
let projectiles_fb = builder.create_vector(&projectiles_fb_vec);
let mut pickups_fb_vec = Vec::new();
if let Some(aoi_entry) = self.player_aois.get(peer_id_str) {
let p_aoi = aoi_entry.value();
let pickups_guard = self.pickups.read();
for pickup_id in p_aoi.visible_pickups.iter() {
if let Some(pickup) = pickups_guard.iter().find(|p| p.id == *pickup_id) {
if pickup.is_active {
let (fb_pickup_type, fb_weapon_type_opt) = map_core_pickup_to_fb(&pickup.pickup_type);
let id_fb = builder.create_string(&pickup.id.to_string());
pickups_fb_vec.push(fb::Pickup::create(&mut builder, &fb::PickupArgs {
id: Some(id_fb), x: pickup.x, y: pickup.y, pickup_type: fb_pickup_type,
weapon_type: fb_weapon_type_opt.unwrap_or(fb::WeaponType::Pistol),
is_active: pickup.is_active,
}));
}
}
}
}
let pickups_fb = builder.create_vector(&pickups_fb_vec);
let player_id_fb_initial = builder.create_string(peer_id_str);
let match_info_guard = self.match_info.read();
let fb_team_scores_vec: Vec<_> = match_info_guard.team_scores.iter().map(|(team_id, score)| {
fb::TeamScoreEntry::create(&mut builder, &fb::TeamScoreEntryArgs { team_id: *team_id as i8, score: *score })
}).collect();
let team_scores_fb = builder.create_vector(&fb_team_scores_vec);
let fb_flag_states_vec: Vec<_> = match_info_guard.flag_states.values().map(|fs| {
let carrier_id_fb = fs.carrier_id.as_ref().map(|id| builder.create_string(id.as_str()));
let pos_fb = fb::Vec2::create(&mut builder, &fb::Vec2Args{ x: fs.position.x, y: fs.position.y });
fb::FlagState::create(&mut builder, &fb::FlagStateArgs {
team_id: fs.team_id as i8, status: fs.status, position: Some(pos_fb),
carrier_id: carrier_id_fb, respawn_timer: fs.respawn_timer,
})
}).collect();
let flag_states_fb = builder.create_vector(&fb_flag_states_vec);
let match_info_fb = fb::MatchInfo::create(&mut builder, &fb::MatchInfoArgs {
time_remaining: match_info_guard.time_remaining,
match_state: match_info_guard.match_state,
winner_id: None,
winner_name: None,
game_mode: match_info_guard.game_mode,
team_scores: Some(team_scores_fb),
});
drop(match_info_guard);
let map_name_fb = builder.create_string("Massive Arena 10v10");
let timestamp_initial = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_millis() as u64;
let initial_state_args = fb::InitialStateMessageArgs {
player_id: Some(player_id_fb_initial),
walls: Some(walls_fb),
players: Some(players_fb),
projectiles: Some(projectiles_fb),
pickups: Some(pickups_fb),
match_info: Some(match_info_fb),
flag_states: Some(flag_states_fb),
timestamp: timestamp_initial,
map_name: Some(map_name_fb),
};
let initial_state_msg = fb::InitialStateMessage::create(&mut builder, &initial_state_args);
let game_msg_args = fb::GameMessageArgs {
msg_type: fb::MessageType::InitialState,
actual_message_type: fb::MessagePayload::InitialStateMessage,
actual_message: Some(initial_state_msg.as_union_value()),
};
let game_msg = fb::GameMessage::create(&mut builder, &game_msg_args);
builder.finish(game_msg, None);
let data_bytes = Bytes::from(builder.finished_data().to_vec());
let dc_clone = Arc::clone(data_channel);
let peer_id_clone = peer_id_str.to_string();
tokio::spawn(async move {
if let Err(e) = dc_clone.send(&data_bytes).await {
handle_dc_send_error(&e.to_string(), &peer_id_clone, "initial state");
}
});
client_state.known_walls_sent = true;
client_state.last_known_player_states.clear();
if let Some(aoi_entry) = self.player_aois.get(peer_id_str) {
let p_aoi = aoi_entry.value();
if let Some(self_pstate_guard) = self.player_manager.get_player_state(&self_player_id_arc) {
client_state.last_known_player_states.insert(self_player_id_arc.clone(), (*self_pstate_guard).clone());
}
for visible_player_id in p_aoi.visible_players.iter() {
if let Some(pstate_guard) = self.player_manager.get_player_state(visible_player_id) {
client_state.last_known_player_states.insert(visible_player_id.clone(), (*pstate_guard).clone());
}
}
} else {
if let Some(self_pstate_guard) = self.player_manager.get_player_state(&self_player_id_arc) {
client_state.last_known_player_states.insert(self_player_id_arc.clone(), (*self_pstate_guard).clone());
}
}
client_state.last_known_projectile_ids.clear();
if let Some(aoi_entry) = self.player_aois.get(peer_id_str) {
aoi_entry.value().visible_projectiles.iter().for_each(|id| { client_state.last_known_projectile_ids.insert(*id); });
}
client_state.last_known_pickup_states.clear();
if let Some(aoi_entry) = self.player_aois.get(peer_id_str) {
let p_aoi = aoi_entry.value();
let pickups_guard = self.pickups.read();
for pickup_id in p_aoi.visible_pickups.iter() {
if let Some(pickup) = pickups_guard.iter().find(|p| p.id == *pickup_id) {
client_state.last_known_pickup_states.insert(*pickup_id, PickupState { is_active: pickup.is_active });
}
}
}
let current_match_info_for_state = self.match_info.read();
client_state.last_known_match_state = Some(current_match_info_for_state.match_state);
client_state.last_known_match_time_remaining = Some(current_match_info_for_state.time_remaining);
client_state.last_known_team_scores = current_match_info_for_state.team_scores.clone(); client_state.known_destroyed_wall_ids.clear(); info!("[{}] Initial state sent successfully", peer_id_str);
}
async fn send_delta_state_to_client(
&self,
peer_id_str: &str,
data_channel: &Arc<crate::core::types::RTCDataChannel>,
client_state: &mut ClientState,
events_to_broadcast: &[GameEvent],
destroyed_wall_ids_snapshot: &[EntityId],
current_timestamp_ms: u64
) {
let mut builder = flatbuffers::FlatBufferBuilder::with_capacity(8192);
let self_player_id_arc = self.player_manager.id_pool.get_or_create(peer_id_str);
let mut players_delta_fb_vec = Vec::new();
let mut player_fields_mask_fb_vec = Vec::new();
let mut last_processed_input_for_client = 0;
if let Some(self_player_state_guard) = self.player_manager.get_player_state(&self_player_id_arc) {
let self_player_state = &*self_player_state_guard;
last_processed_input_for_client = self_player_state.last_processed_input_sequence;
if self_player_state.changed_fields > 0 || client_state.last_known_player_states.get(&self_player_id_arc).map_or(true, |old| old.changed_fields == 0xFFFF) {
players_delta_fb_vec.push(create_fb_player_state_for_delta(&mut builder, self_player_state, self_player_state.changed_fields));
player_fields_mask_fb_vec.push(self_player_state.changed_fields as u8);
client_state.last_known_player_states.insert(self_player_id_arc.clone(), self_player_state.clone());
}
}
if let Some(aoi_entry) = self.player_aois.get(peer_id_str) {
let p_aoi = aoi_entry.value();
for visible_player_id in p_aoi.visible_players.iter() {
if visible_player_id == &self_player_id_arc { continue; }
if let Some(other_pstate_guard) = self.player_manager.get_player_state(visible_player_id) {
let other_pstate = &*other_pstate_guard;
if other_pstate.changed_fields > 0 ||
client_state.last_known_player_states.get(visible_player_id).map_or(true, |old_ps| *old_ps != *other_pstate) {
players_delta_fb_vec.push(create_fb_player_state_for_delta(&mut builder, other_pstate, other_pstate.changed_fields));
player_fields_mask_fb_vec.push(other_pstate.changed_fields as u8);
client_state.last_known_player_states.insert(visible_player_id.clone(), other_pstate.clone());
}
}
}
client_state.last_known_player_states.retain(|id, _| id == &self_player_id_arc || p_aoi.visible_players.contains(id));
} else {
client_state.last_known_player_states.retain(|id, _| id == &self_player_id_arc);
}
let players_fb = builder.create_vector(&players_delta_fb_vec);
let player_fields_mask_fb = builder.create_vector(&player_fields_mask_fb_vec);
let mut new_projectiles_fb_vec = Vec::new();
let mut removed_projectiles_ids_str_vec = Vec::new();
if let Some(aoi_entry) = self.player_aois.get(peer_id_str) {
let p_aoi = aoi_entry.value();
let projectiles_read_guard = self.projectiles.read();
let current_projectiles_in_aoi: HashSet<EntityId> = p_aoi.visible_projectiles.clone();
for proj_id in current_projectiles_in_aoi.iter() {
if !client_state.last_known_projectile_ids.contains(proj_id) {
if let Some(proj) = projectiles_read_guard.iter().find(|p| p.id == *proj_id) {
let id_fb = builder.create_string(&proj.id.to_string());
let owner_id_fb = builder.create_string(proj.owner_id.as_str());
new_projectiles_fb_vec.push(fb::ProjectileState::create(&mut builder, &fb::ProjectileStateArgs{
id: Some(id_fb), x: proj.x, y: proj.y, owner_id: Some(owner_id_fb),
weapon_type: map_server_weapon_to_fb(proj.weapon_type),
velocity_x: proj.velocity_x, velocity_y: proj.velocity_y,
}));
}
}
}
for known_proj_id in client_state.last_known_projectile_ids.iter() {
if !current_projectiles_in_aoi.contains(known_proj_id) {
removed_projectiles_ids_str_vec.push(known_proj_id.to_string());
}
}
client_state.last_known_projectile_ids = current_projectiles_in_aoi;
} else {
for known_proj_id in client_state.last_known_projectile_ids.iter() {
removed_projectiles_ids_str_vec.push(known_proj_id.to_string());
}
client_state.last_known_projectile_ids.clear();
}
let projectiles_delta_fb = builder.create_vector(&new_projectiles_fb_vec);
let removed_projectiles_fb_offsets: Vec<_> = removed_projectiles_ids_str_vec.iter().map(|s| builder.create_string(s)).collect();
let removed_projectiles_fb = builder.create_vector(&removed_projectiles_fb_offsets);
let mut changed_pickups_fb_vec = Vec::new();
let mut deactivated_pickup_ids_str_vec = Vec::new();
if let Some(aoi_entry) = self.player_aois.get(peer_id_str) {
let p_aoi = aoi_entry.value();
let pickups_read_guard = self.pickups.read();
let mut current_pickups_in_aoi_states = HashMap::new();
for pickup_id_in_aoi in p_aoi.visible_pickups.iter() {
if let Some(pickup) = pickups_read_guard.iter().find(|p| p.id == *pickup_id_in_aoi) {
current_pickups_in_aoi_states.insert(*pickup_id_in_aoi, PickupState { is_active: pickup.is_active });
let last_known_active_opt = client_state.last_known_pickup_states.get(pickup_id_in_aoi);
if last_known_active_opt.map_or(true, |last_state| last_state.is_active != pickup.is_active) {
let id_fb = builder.create_string(&pickup.id.to_string());
let (pt, wt_opt) = map_core_pickup_to_fb(&pickup.pickup_type);
changed_pickups_fb_vec.push(fb::Pickup::create(&mut builder, &fb::PickupArgs{
id: Some(id_fb), x: pickup.x, y: pickup.y, pickup_type: pt,
weapon_type: wt_opt.unwrap_or(fb::WeaponType::Pistol),
is_active: pickup.is_active,
}));
}
}
}
for (known_pickup_id, was_active_ref) in client_state.last_known_pickup_states.iter() {
let is_still_in_aoi_and_active = current_pickups_in_aoi_states.get(known_pickup_id)
.map_or(false, |pickup_state_ref| pickup_state_ref.is_active); if was_active_ref.is_active && !is_still_in_aoi_and_active {
deactivated_pickup_ids_str_vec.push(known_pickup_id.to_string());
}
}
client_state.last_known_pickup_states = current_pickups_in_aoi_states;
} else {
for (known_pickup_id, was_active) in client_state.last_known_pickup_states.iter() {
if was_active.is_active { deactivated_pickup_ids_str_vec.push(known_pickup_id.to_string()); }
}
client_state.last_known_pickup_states.clear();
}
let pickups_delta_fb = builder.create_vector(&changed_pickups_fb_vec);
let deactivated_pickups_fb_offsets: Vec<_> = deactivated_pickup_ids_str_vec.iter().map(|s| builder.create_string(s)).collect();
let deactivated_pickups_fb = builder.create_vector(&deactivated_pickups_fb_offsets);
let game_events_fb_vec: Vec<_> = events_to_broadcast.iter().map(|event| {
let event_pos = event_position(event);
let pos_fb_offset = fb::Vec2::create(&mut builder, &fb::Vec2Args { x: event_pos.x, y: event_pos.y });
let instigator_id_fb = event_instigator_id(event).map(|id| builder.create_string(id.as_str()));
let target_id_fb = event_target_id(event).map(|id_str| builder.create_string(&id_str));
let weapon_type_fb = event_weapon_type(event).map_or(fb::WeaponType::Pistol, map_server_weapon_to_fb);
fb::GameEvent::create(&mut builder, &fb::GameEventArgs{
event_type: map_game_event_type_to_fb(event),
position: Some(pos_fb_offset),
instigator_id: instigator_id_fb,
target_id: target_id_fb,
weapon_type: weapon_type_fb,
value: event_value(event).unwrap_or(0.0),
})
}).collect();
let game_events_fb = builder.create_vector(&game_events_fb_vec);
let current_match_info_guard = self.match_info.read();
let team_scores_changed = client_state.last_known_team_scores != current_match_info_guard.team_scores;
let match_info_fb_offset = if client_state.last_known_match_state != Some(current_match_info_guard.match_state) ||
client_state.last_known_match_time_remaining.map_or(true, |t| (t - current_match_info_guard.time_remaining).abs() > 0.5) ||
team_scores_changed {
client_state.last_known_match_state = Some(current_match_info_guard.match_state);
client_state.last_known_match_time_remaining = Some(current_match_info_guard.time_remaining);
client_state.last_known_team_scores = current_match_info_guard.team_scores.clone();
let fb_team_scores_vec_delta: Vec<_> = current_match_info_guard.team_scores.iter().map(|(team_id_ref, score_ref)| {
fb::TeamScoreEntry::create(&mut builder, &fb::TeamScoreEntryArgs { team_id: *team_id_ref as i8, score: *score_ref })
}).collect();
let team_scores_fb_delta = builder.create_vector(&fb_team_scores_vec_delta);
let mut winner_id_fb = None;
let mut winner_name_fb = None;
if current_match_info_guard.match_state == fb::MatchStateType::Ended {
if current_match_info_guard.game_mode == fb::GameModeType::TeamDeathmatch || current_match_info_guard.game_mode == fb::GameModeType::CaptureTheFlag {
let t1_score = current_match_info_guard.team_scores.get(&1).cloned().unwrap_or(0);
let t2_score = current_match_info_guard.team_scores.get(&2).cloned().unwrap_or(0);
if t1_score > t2_score {
winner_id_fb = Some(builder.create_string("1"));
winner_name_fb = Some(builder.create_string("Red Team"));
} else if t2_score > t1_score {
winner_id_fb = Some(builder.create_string("2"));
winner_name_fb = Some(builder.create_string("Blue Team"));
} else if t1_score != 0 || t2_score != 0 {
winner_name_fb = Some(builder.create_string("Draw"));
}
}
}
Some(fb::MatchInfo::create(&mut builder, &fb::MatchInfoArgs{
time_remaining: current_match_info_guard.time_remaining,
match_state: current_match_info_guard.match_state,
winner_id: winner_id_fb,
winner_name: winner_name_fb,
game_mode: current_match_info_guard.game_mode,
team_scores: Some(team_scores_fb_delta),
}))
} else { None };
let flag_states_delta_fb_vec: Vec<_> = if match_info_fb_offset.is_some() && current_match_info_guard.game_mode == fb::GameModeType::CaptureTheFlag {
current_match_info_guard.flag_states.values().map(|fs| {
let carrier_id_fb = fs.carrier_id.as_ref().map(|id| builder.create_string(id.as_str()));
let pos_fb = fb::Vec2::create(&mut builder, &fb::Vec2Args{ x: fs.position.x, y: fs.position.y });
fb::FlagState::create(&mut builder, &fb::FlagStateArgs {
team_id: fs.team_id as i8, status: fs.status, position: Some(pos_fb),
carrier_id: carrier_id_fb, respawn_timer: fs.respawn_timer,
})
}).collect()
} else { Vec::new() };
let flag_states_delta_fb = if !flag_states_delta_fb_vec.is_empty() { Some(builder.create_vector(&flag_states_delta_fb_vec)) } else { None };
drop(current_match_info_guard);
let kill_feed_guard = self.kill_feed.read();
let new_kill_feed_entries_to_send = kill_feed_guard.iter()
.skip(client_state.last_kill_feed_count_sent)
.map(|kf_entry| {
let killer_fb = builder.create_string(&kf_entry.killer_name);
let victim_fb = builder.create_string(&kf_entry.victim_name);
fb::KillFeedEntry::create(&mut builder, &fb::KillFeedEntryArgs{
killer_name: Some(killer_fb), victim_name: Some(victim_fb),
weapon: map_server_weapon_to_fb(kf_entry.weapon), timestamp: kf_entry.timestamp as f32,
killer_position: None, victim_position: None, is_headshot: false,
})
}).collect::<Vec<_>>();
client_state.last_kill_feed_count_sent = kill_feed_guard.len();
drop(kill_feed_guard);
let kill_feed_fb = if !new_kill_feed_entries_to_send.is_empty() { Some(builder.create_vector(&new_kill_feed_entries_to_send)) } else { None };
let mut new_destroyed_wall_ids_for_client_update_set = Vec::new();
let new_destroyed_wall_ids_fb_vec: Vec<_> = destroyed_wall_ids_snapshot.iter()
.filter(|wall_id_ref| !client_state.known_destroyed_wall_ids.contains(*wall_id_ref))
.map(|&wall_id_val| {
new_destroyed_wall_ids_for_client_update_set.push(wall_id_val);
builder.create_string(&wall_id_val.to_string())
})
.collect();
for wall_id_to_add in new_destroyed_wall_ids_for_client_update_set {
client_state.known_destroyed_wall_ids.insert(wall_id_to_add);
}
let destroyed_walls_fb = if !new_destroyed_wall_ids_fb_vec.is_empty() { Some(builder.create_vector(&new_destroyed_wall_ids_fb_vec)) } else { None };
let updated_walls_read_guard = self.updated_walls_this_tick.read();
let mut updated_walls_fb_vec = Vec::new();
if let Some(aoi_entry) = self.player_aois.get(peer_id_str) {
let p_aoi = aoi_entry.value();
for (wall_id, wall_data) in updated_walls_read_guard.iter() {
if p_aoi.visible_walls.contains(wall_id) {
let id_fb = builder.create_string(&wall_data.id.to_string());
updated_walls_fb_vec.push(fb::Wall::create(&mut builder, &fb::WallArgs{
id: Some(id_fb),
x: wall_data.x, y: wall_data.y,
width: wall_data.width, height: wall_data.height,
is_destructible: wall_data.is_destructible,
current_health: wall_data.current_health,
max_health: wall_data.max_health,
}));
client_state.known_destroyed_wall_ids.remove(wall_id); }
}
}
let updated_walls_fb = if !updated_walls_fb_vec.is_empty() {
Some(builder.create_vector(&updated_walls_fb_vec))
} else {
None
};
drop(updated_walls_read_guard);
let mut removed_player_ids_fb_offsets_vec = Vec::new();
let mut player_ids_to_remove_from_client_state_tracking = HashSet::new();
for known_player_id_arc in client_state.last_known_player_states.keys() {
if !self.player_manager.get_player_state(known_player_id_arc).is_some() {
removed_player_ids_fb_offsets_vec.push(builder.create_string(known_player_id_arc.as_str()));
player_ids_to_remove_from_client_state_tracking.insert(known_player_id_arc.clone());
}
}
for id_to_remove in player_ids_to_remove_from_client_state_tracking {
client_state.last_known_player_states.remove(&id_to_remove);
}
let removed_players_fb = if !removed_player_ids_fb_offsets_vec.is_empty() {
Some(builder.create_vector(&removed_player_ids_fb_offsets_vec))
} else {
None
};
let delta_state_args = fb::DeltaStateMessageArgs {
players: Some(players_fb),
projectiles: Some(projectiles_delta_fb),
removed_projectiles: Some(removed_projectiles_fb),
pickups: Some(pickups_delta_fb),
deactivated_pickup_ids: Some(deactivated_pickups_fb),
game_events: Some(game_events_fb),
timestamp: current_timestamp_ms,
last_processed_input_sequence: last_processed_input_for_client,
changed_player_fields: Some(player_fields_mask_fb),
kill_feed: kill_feed_fb,
match_info: match_info_fb_offset,
destroyed_wall_ids: destroyed_walls_fb,
flag_states: flag_states_delta_fb,
removed_player_ids: removed_players_fb,
updated_walls: updated_walls_fb,
};
let delta_state_msg = fb::DeltaStateMessage::create(&mut builder, &delta_state_args);
let game_msg_args = fb::GameMessageArgs {
msg_type: fb::MessageType::DeltaState,
actual_message_type: fb::MessagePayload::DeltaStateMessage,
actual_message: Some(delta_state_msg.as_union_value()),
};
let game_msg = fb::GameMessage::create(&mut builder, &game_msg_args);
builder.finish(game_msg, None);
let data_bytes = Bytes::from(builder.finished_data().to_vec());
let dc_clone = Arc::clone(data_channel);
let peer_id_clone = peer_id_str.to_string();
tokio::spawn(async move {
if let Err(e) = dc_clone.send(&data_bytes).await {
handle_dc_send_error(&e.to_string(), &peer_id_clone, "delta state");
}
});
}
async fn send_pending_chat_messages(
&self,
peer_id_str: &str,
data_channel: &Arc<crate::core::types::RTCDataChannel>,
client_state: &mut ClientState
) {
let last_seq_sent = client_state.last_chat_message_seq_sent;
let mut max_seq_in_batch = last_seq_sent;
let chat_messages_to_send: Vec<ChatMessage> = {
let chat_guard = self.chat_messages_queue.read().await;
chat_guard.iter()
.filter(|msg| msg.seq > last_seq_sent)
.cloned()
.collect()
};
if !chat_messages_to_send.is_empty() {
for chat_entry in chat_messages_to_send.iter() {
let mut chat_builder = flatbuffers::FlatBufferBuilder::with_capacity(256);
let player_id_fb = chat_builder.create_string(chat_entry.player_id.as_str());
let username_fb = chat_builder.create_string(&chat_entry.username);
let message_fb = chat_builder.create_string(&chat_entry.message);
let chat_payload_offset = fb::ChatMessage::create(&mut chat_builder, &fb::ChatMessageArgs {
seq: chat_entry.seq,
player_id: Some(player_id_fb),
username: Some(username_fb),
message: Some(message_fb),
timestamp: chat_entry.timestamp,
});
let game_message_offset = fb::GameMessage::create(&mut chat_builder, &fb::GameMessageArgs {
msg_type: fb::MessageType::Chat,
actual_message_type: fb::MessagePayload::ChatMessage,
actual_message: Some(chat_payload_offset.as_union_value()),
});
chat_builder.finish(game_message_offset, None);
let chat_msg_bytes = Bytes::from(chat_builder.finished_data().to_vec());
let dc_for_chat = Arc::clone(data_channel);
let peer_id_for_log = peer_id_str.to_string();
let current_seq = chat_entry.seq;
tokio::spawn(async move {
if let Err(e) = dc_for_chat.send(&chat_msg_bytes).await {
handle_dc_send_error(&e.to_string(), &peer_id_for_log, &format!("chat message seq {}", current_seq));
}
});
if chat_entry.seq > max_seq_in_batch {
max_seq_in_batch = chat_entry.seq;
}
}
client_state.last_chat_message_seq_sent = max_seq_in_batch;
}
}
pub async fn process_game_tick(self: Arc<Self>, dt: f32) -> Result<(), ServerError> {
let tick_started = Instant::now();
let frame = self.frame_counter.load(AtomicOrdering::Relaxed);
let stage1_start = Instant::now();
let mut set = JoinSet::new();
set.spawn({
let server_clone = Arc::clone(&self);
async move {
let task_name = "network_input";
trace!("[Frame {}] Starting task: {}", frame, task_name);
let result = timeout(Duration::from_millis(NET_IO_TIMEOUT_MS), async {
server_clone.process_network_input().await;
}).await;
if result.is_err() {
if frame % 60 == 0 {
warn!("[Frame {}] Task '{}' timed out after {}ms", frame, task_name, NET_IO_TIMEOUT_MS);
}
}
trace!("[Frame {}] Finished task: {}", frame, task_name);
}
});
if frame % AI_UPDATE_STRIDE == 0 {
set.spawn({
let server_clone = Arc::clone(&self);
async move {
let task_name = "ai_update";
trace!("[Frame {}] Starting task: {}", frame, task_name);
let result = timeout(Duration::from_millis(AI_TIMEOUT_MS), async {
server_clone.run_ai_update().await;
}).await;
if result.is_err() {
if frame % 60 == 0 {
warn!("[Frame {}] Task '{}' timed out after {}ms", frame, task_name, AI_TIMEOUT_MS);
}
}
trace!("[Frame {}] Finished task: {}", frame, task_name);
}
});
}
while let Some(res) = set.join_next().await {
if let Err(e) = res {
error!("[Frame {}] Task join error in Stage 1: {}", frame, e);
}
}
let stage1_elapsed = stage1_start.elapsed();
trace!("[Frame {}] Stage 1 (Input/AI) took: {:?}", frame, stage1_elapsed);
let stage2_start = Instant::now();
let physics_start = Instant::now();
self.run_physics_update(dt).await;
let physics_elapsed = physics_start.elapsed();
trace!("[Frame {}] Physics update took: {:?}", frame, physics_elapsed);
let game_logic_start = Instant::now();
self.run_game_logic_update(dt).await;
let game_logic_elapsed = game_logic_start.elapsed();
trace!("[Frame {}] Game logic update took: {:?}", frame, game_logic_elapsed);
let stage2_elapsed = stage2_start.elapsed();
if stage2_elapsed > Duration::from_millis(SLOW_TICK_LOG_MS) && frame % 60 == 0 {
warn!(
?frame,
ms = stage2_elapsed.as_micros() as f64 / 1000.0,
physics_ms = physics_elapsed.as_micros() as f64 / 1000.0,
game_logic_ms = game_logic_elapsed.as_micros() as f64 / 1000.0,
"Stage 2 (Physics/Logic) exceeded soft budget {}ms", SLOW_TICK_LOG_MS
);
}
let stage3_start = Instant::now();
let sync_start = Instant::now();
self.synchronize_state().await;
let sync_elapsed = sync_start.elapsed();
trace!("[Frame {}] State synchronization took: {:?}", frame, sync_elapsed);
let broadcast_start_time = Instant::now();
let broadcast_elapsed_duration;
let broadcast_timed_out_flag;
{
let server_for_broadcast_call = Arc::clone(&self);
let broadcast_future = server_for_broadcast_call.broadcast_world_updates_optimized();
let timed_broadcast_future = tokio::time::timeout(
Duration::from_millis(FAN_OUT_TIMEOUT_MS),
broadcast_future
);
let b_start_inner = Instant::now();
broadcast_timed_out_flag = timed_broadcast_future.await.is_err();
broadcast_elapsed_duration = b_start_inner.elapsed();
}
trace!("[Frame {}] Broadcast took: {:?} (timed_out: {})", frame, broadcast_elapsed_duration, broadcast_timed_out_flag);
if broadcast_timed_out_flag {
if frame % 60 == 0 {
error!("[Frame {}] Broadcast stage timed out after {}ms (actual: {:?})", frame, FAN_OUT_TIMEOUT_MS, broadcast_elapsed_duration);
}
}
let _stage3_elapsed = stage3_start.elapsed();
self.destroyed_wall_ids_this_tick.write().clear();
self.updated_walls_this_tick.write().clear();
trace!("[Frame {}] Tick-local cleanup complete.", frame);
let total_tick_processing_elapsed = tick_started.elapsed();
if total_tick_processing_elapsed > Duration::from_millis(TARGET_TICK_MS + 4) {
if frame % 10 == 0 {
warn!(
"Frame {} timing breakdown:\n\
Total: {:.2}ms\n\
- Input/AI (Stage 1): {:.2}ms\n\
- Physics (Stage 2a): {:.2}ms\n\
- Game Logic (Stage 2b): {:.2}ms\n\
- State Sync (Stage 3a): {:.2}ms\n\
- Broadcast (Stage 3b): {:.2}ms (timed_out: {})\n\
(Target Tick: {}ms)",
frame,
total_tick_processing_elapsed.as_secs_f32() * 1000.0,
stage1_elapsed.as_secs_f32() * 1000.0,
physics_elapsed.as_secs_f32() * 1000.0,
game_logic_elapsed.as_secs_f32() * 1000.0,
sync_elapsed.as_secs_f32() * 1000.0,
broadcast_elapsed_duration.as_secs_f32() * 1000.0,
broadcast_timed_out_flag,
TARGET_TICK_MS
);
}
}
if total_tick_processing_elapsed > Duration::from_millis(TARGET_TICK_MS) {
if frame % 60 == 0 {
warn!(
?frame,
ms = total_tick_processing_elapsed.as_micros() as f64 / 1000.0,
target = TARGET_TICK_MS,
"Tick processing WORK exceeded hard budget (game_loop will log wall-clock overrun)"
);
}
}
Ok(())
}
fn fb_safe_str(s: &str) -> String {
s.chars()
.filter(|&c| c != '\0' && c.is_ascii())
.take(255) .collect()
}
async fn send_chat_messages_optimized(
&self,
peer_id_str: &str,
data_channel: &Arc<crate::core::types::RTCDataChannel>,
client_state: &ClientState,
chat_messages: &[ChatMessage],
) {
let mut client_state_copy = client_state.clone();
Self::send_chat_messages_static(
peer_id_str,
data_channel,
&mut client_state_copy,
chat_messages,
&self.chat_messages_queue,
).await;
self.client_states_map.write().insert(peer_id_str.to_string(), client_state_copy);
}
}
fn event_position(event: &GameEvent) -> Vec2 {
match event {
GameEvent::PlayerDamaged { position, .. } => *position,
GameEvent::PlayerKilled { position, .. } => *position,
GameEvent::ProjectileHitWall { position, .. } => *position,
GameEvent::PowerupCollected { position, .. } => *position,
GameEvent::WeaponFired { position, .. } => *position,
GameEvent::WallDestroyed { position, .. } => *position,
GameEvent::WallImpact { position, .. } => *position,
GameEvent::MeleeHit { position, .. } => *position,
GameEvent::Footstep { position, .. } => *position,
GameEvent::FlagGrabbed { position, .. } => *position,
GameEvent::FlagDropped { position, .. } => *position,
GameEvent::FlagReturned { position, .. } => *position,
GameEvent::FlagCaptured { position, .. } => *position,
_ => Vec2::zero(),
}
}
fn event_instigator_id(event: &GameEvent) -> Option<PlayerID> {
match event {
GameEvent::PlayerDamaged { attacker_id, .. } => attacker_id.clone(),
GameEvent::PlayerKilled { killer_id, .. } => Some(killer_id.clone()),
GameEvent::WeaponFired { player_id, ..} => Some(player_id.clone()),
GameEvent::PowerupCollected { player_id, .. } => Some(player_id.clone()),
GameEvent::FlagGrabbed { player_id, .. } => Some(player_id.clone()),
GameEvent::FlagCaptured { capturer_id, .. } => Some(capturer_id.clone()),
_ => None,
}
}
fn event_target_id(event: &GameEvent) -> Option<String> {
match event {
GameEvent::PlayerDamaged { target_id, .. } => Some(target_id.to_string()),
GameEvent::PlayerKilled { victim_id, .. } => Some(victim_id.to_string()),
GameEvent::ProjectileHitWall { wall_id, .. } => Some(wall_id.to_string()),
GameEvent::PowerupCollected { pickup_id, ..} => Some(pickup_id.to_string()),
GameEvent::WallDestroyed { wall_id, .. } => Some(wall_id.to_string()),
GameEvent::WallImpact { wall_id, .. } => Some(wall_id.to_string()),
GameEvent::MeleeHit { target_id, .. } => target_id.as_ref().map(|id| id.to_string()),
GameEvent::FlagDropped { flag_team_id, .. } => Some(flag_team_id.to_string()),
GameEvent::FlagReturned { flag_team_id, .. } => Some(flag_team_id.to_string()),
_ => None,
}
}
fn event_weapon_type(event: &GameEvent) -> Option<ServerWeaponType> {
match event {
GameEvent::PlayerDamaged { weapon, .. } => Some(*weapon),
GameEvent::PlayerKilled { weapon, .. } => Some(*weapon),
GameEvent::WeaponFired { weapon, .. } => Some(*weapon),
_ => None,
}
}
fn event_value(event: &GameEvent) -> Option<f32> {
match event {
GameEvent::PlayerDamaged { damage, .. } => Some(*damage as f32),
GameEvent::WallImpact { damage, .. } => Some(*damage as f32),
_ => None,
}
}
fn map_game_event_type_to_fb(event: &GameEvent) -> fb::GameEventType {
match event {
GameEvent::PlayerDamaged { .. } => fb::GameEventType::PlayerDamageEffect,
GameEvent::PlayerKilled { .. } => fb::GameEventType::PlayerDamageEffect,
GameEvent::ProjectileHitWall { .. } => fb::GameEventType::WallImpact,
GameEvent::PowerupCollected { .. } => fb::GameEventType::PowerupActivated,
GameEvent::WeaponFired { .. } => fb::GameEventType::WeaponFire,
GameEvent::WallDestroyed { .. } => fb::GameEventType::WallDestroyed,
GameEvent::WallImpact { .. } => fb::GameEventType::WallImpact,
GameEvent::FlagGrabbed { .. } => fb::GameEventType::FlagGrabbed,
GameEvent::FlagDropped { .. } => fb::GameEventType::FlagDropped,
GameEvent::FlagReturned { .. } => fb::GameEventType::FlagReturned,
GameEvent::FlagCaptured { .. } => fb::GameEventType::FlagCaptured,
GameEvent::PlayerJoined { .. } | GameEvent::PlayerLeft { .. } => fb::GameEventType::BulletImpact, GameEvent::MeleeHit { .. } => fb::GameEventType::PlayerDamageEffect, GameEvent::Footstep { .. } => fb::GameEventType::BulletImpact, }
}