1use std::collections::{HashMap, VecDeque};
5use std::time::{Duration, Instant};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub struct LobbyId(pub u64);
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub struct PlayerId(pub u64);
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub struct TeamId(pub u8);
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23pub struct GameMode(pub u8);
24
25impl GameMode {
26 pub const DEATHMATCH: Self = Self(0);
27 pub const TEAM_VS_TEAM: Self = Self(1);
28 pub const CAPTURE_FLAG: Self = Self(2);
29 pub const KING_HILL: Self = Self(3);
30 pub const CUSTOM: Self = Self(0xFF);
31}
32
33#[derive(Debug, Clone, PartialEq)]
37pub enum LobbyState {
38 Waiting,
40 Countdown(f32),
42 InGame,
44 Postgame,
46}
47
48impl LobbyState {
49 pub fn is_joinable(&self) -> bool {
50 matches!(self, LobbyState::Waiting)
51 }
52}
53
54#[derive(Debug, Clone)]
58pub struct LobbyConfig {
59 pub max_players: u8,
60 pub min_players: u8,
61 pub game_mode: GameMode,
62 pub map_id: u32,
63 pub password: String,
65 pub public: bool,
66 pub ranked: bool,
67 pub countdown_secs: f32,
69}
70
71impl Default for LobbyConfig {
72 fn default() -> Self {
73 Self {
74 max_players: 8,
75 min_players: 2,
76 game_mode: GameMode::DEATHMATCH,
77 map_id: 0,
78 password: String::new(),
79 public: true,
80 ranked: false,
81 countdown_secs: 10.0,
82 }
83 }
84}
85
86impl LobbyConfig {
87 pub fn has_password(&self) -> bool { !self.password.is_empty() }
88}
89
90#[derive(Debug, Clone)]
94pub struct LobbyPlayer {
95 pub id: PlayerId,
96 pub name: String,
97 pub ready: bool,
98 pub team: Option<TeamId>,
99 pub ping_ms: u32,
100 pub spectator: bool,
101}
102
103impl LobbyPlayer {
104 pub fn new(id: PlayerId, name: impl Into<String>) -> Self {
105 Self {
106 id, name: name.into(), ready: false,
107 team: None, ping_ms: 0, spectator: false,
108 }
109 }
110}
111
112#[derive(Debug)]
116pub struct Lobby {
117 pub id: LobbyId,
118 pub name: String,
119 pub host_id: PlayerId,
120 pub players: Vec<LobbyPlayer>,
121 pub state: LobbyState,
122 pub config: LobbyConfig,
123 created_at: Instant,
124 countdown_started: Option<Instant>,
125}
126
127impl Lobby {
128 pub fn new(id: LobbyId, name: impl Into<String>, host_id: PlayerId, config: LobbyConfig) -> Self {
129 Self {
130 id, name: name.into(), host_id,
131 players: Vec::new(), state: LobbyState::Waiting,
132 config, created_at: Instant::now(), countdown_started: None,
133 }
134 }
135
136 pub fn has_room(&self) -> bool {
138 self.players.len() < self.config.max_players as usize
139 }
140
141 pub fn has_min_players(&self) -> bool {
143 self.players.len() >= self.config.min_players as usize
144 }
145
146 pub fn all_ready(&self) -> bool {
147 !self.players.is_empty()
148 && self.players.iter().filter(|p| !p.spectator).all(|p| p.ready)
149 }
150
151 pub fn player(&self, id: PlayerId) -> Option<&LobbyPlayer> {
152 self.players.iter().find(|p| p.id == id)
153 }
154
155 pub fn player_mut(&mut self, id: PlayerId) -> Option<&mut LobbyPlayer> {
156 self.players.iter_mut().find(|p| p.id == id)
157 }
158
159 pub fn contains(&self, id: PlayerId) -> bool {
160 self.players.iter().any(|p| p.id == id)
161 }
162
163 pub fn tick_countdown(&mut self, dt: f32) -> bool {
165 if let LobbyState::Countdown(ref mut t) = self.state {
166 *t -= dt;
167 if *t <= 0.0 {
168 return true;
169 }
170 }
171 false
172 }
173
174 pub fn player_count(&self) -> usize { self.players.len() }
175 pub fn spectator_count(&self) -> usize { self.players.iter().filter(|p| p.spectator).count() }
176 pub fn active_count(&self) -> usize { self.players.iter().filter(|p| !p.spectator).count() }
177}
178
179#[derive(Debug, Clone, PartialEq, Eq)]
182pub enum LobbyError {
183 LobbyNotFound(LobbyId),
184 LobbyFull,
185 LobbyNotJoinable,
186 WrongPassword,
187 PlayerNotFound(PlayerId),
188 PlayerAlreadyInLobby,
189 NotHost,
190 NotEnoughPlayers,
191 AlreadyInGame,
192 CannotKickSelf,
193 TeamNotFound(TeamId),
194 MatchmakingError(String),
195}
196
197impl std::fmt::Display for LobbyError {
198 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
199 write!(f, "{self:?}")
200 }
201}
202
203impl std::error::Error for LobbyError {}
204
205pub struct LobbyManager {
209 lobbies: HashMap<LobbyId, Lobby>,
210 player_lobby: HashMap<PlayerId, LobbyId>,
212 next_lobby_id: u64,
213}
214
215impl LobbyManager {
216 pub fn new() -> Self {
217 Self {
218 lobbies: HashMap::new(),
219 player_lobby: HashMap::new(),
220 next_lobby_id: 1,
221 }
222 }
223
224 fn alloc_id(&mut self) -> LobbyId {
225 let id = LobbyId(self.next_lobby_id);
226 self.next_lobby_id += 1;
227 id
228 }
229
230 pub fn create_lobby(
232 &mut self,
233 host_id: PlayerId,
234 host_name: impl Into<String>,
235 name: impl Into<String>,
236 config: LobbyConfig,
237 ) -> Result<LobbyId, LobbyError> {
238 if self.player_lobby.contains_key(&host_id) {
240 return Err(LobbyError::PlayerAlreadyInLobby);
241 }
242 let id = self.alloc_id();
243 let mut lobby = Lobby::new(id, name, host_id, config);
244 lobby.players.push(LobbyPlayer::new(host_id, host_name));
245 self.player_lobby.insert(host_id, id);
246 self.lobbies.insert(id, lobby);
247 Ok(id)
248 }
249
250 pub fn destroy_lobby(&mut self, id: LobbyId) -> Result<(), LobbyError> {
252 let lobby = self.lobbies.remove(&id).ok_or(LobbyError::LobbyNotFound(id))?;
253 for p in &lobby.players {
254 self.player_lobby.remove(&p.id);
255 }
256 Ok(())
257 }
258
259 pub fn join(
261 &mut self,
262 player_id: PlayerId,
263 player_name: impl Into<String>,
264 lobby_id: LobbyId,
265 password: &str,
266 ) -> Result<(), LobbyError> {
267 if self.player_lobby.contains_key(&player_id) {
268 return Err(LobbyError::PlayerAlreadyInLobby);
269 }
270 let lobby = self.lobbies.get_mut(&lobby_id).ok_or(LobbyError::LobbyNotFound(lobby_id))?;
271 if !lobby.state.is_joinable() {
272 return Err(LobbyError::LobbyNotJoinable);
273 }
274 if !lobby.has_room() {
275 return Err(LobbyError::LobbyFull);
276 }
277 if lobby.config.has_password() && lobby.config.password != password {
278 return Err(LobbyError::WrongPassword);
279 }
280 lobby.players.push(LobbyPlayer::new(player_id, player_name));
281 self.player_lobby.insert(player_id, lobby_id);
282 Ok(())
283 }
284
285 pub fn leave(&mut self, player_id: PlayerId) -> Result<LobbyId, LobbyError> {
287 let lobby_id = self.player_lobby.remove(&player_id)
288 .ok_or(LobbyError::PlayerNotFound(player_id))?;
289 let lobby = self.lobbies.get_mut(&lobby_id)
290 .ok_or(LobbyError::LobbyNotFound(lobby_id))?;
291 lobby.players.retain(|p| p.id != player_id);
292
293 if lobby.host_id == player_id {
295 if let Some(new_host) = lobby.players.first() {
296 lobby.host_id = new_host.id;
297 } else {
298 self.lobbies.remove(&lobby_id);
300 }
301 }
302 Ok(lobby_id)
303 }
304
305 pub fn set_ready(&mut self, player_id: PlayerId, ready: bool) -> Result<(), LobbyError> {
307 let lobby_id = *self.player_lobby.get(&player_id)
308 .ok_or(LobbyError::PlayerNotFound(player_id))?;
309 let lobby = self.lobbies.get_mut(&lobby_id)
310 .ok_or(LobbyError::LobbyNotFound(lobby_id))?;
311 let player = lobby.player_mut(player_id)
312 .ok_or(LobbyError::PlayerNotFound(player_id))?;
313 player.ready = ready;
314 Ok(())
315 }
316
317 pub fn kick(&mut self, host_id: PlayerId, target_id: PlayerId) -> Result<(), LobbyError> {
319 if host_id == target_id {
320 return Err(LobbyError::CannotKickSelf);
321 }
322 let lobby_id = *self.player_lobby.get(&host_id)
323 .ok_or(LobbyError::PlayerNotFound(host_id))?;
324 let lobby = self.lobbies.get(&lobby_id)
325 .ok_or(LobbyError::LobbyNotFound(lobby_id))?;
326 if lobby.host_id != host_id {
327 return Err(LobbyError::NotHost);
328 }
329 if !lobby.contains(target_id) {
330 return Err(LobbyError::PlayerNotFound(target_id));
331 }
332 self.leave(target_id)?;
333 Ok(())
334 }
335
336 pub fn start_game(&mut self, host_id: PlayerId) -> Result<(), LobbyError> {
339 let lobby_id = *self.player_lobby.get(&host_id)
340 .ok_or(LobbyError::PlayerNotFound(host_id))?;
341 let lobby = self.lobbies.get_mut(&lobby_id)
342 .ok_or(LobbyError::LobbyNotFound(lobby_id))?;
343 if lobby.host_id != host_id {
344 return Err(LobbyError::NotHost);
345 }
346 if matches!(lobby.state, LobbyState::InGame) {
347 return Err(LobbyError::AlreadyInGame);
348 }
349 if !lobby.has_min_players() {
350 return Err(LobbyError::NotEnoughPlayers);
351 }
352 let secs = lobby.config.countdown_secs;
353 lobby.state = LobbyState::Countdown(secs);
354 lobby.countdown_started = Some(Instant::now());
355 Ok(())
356 }
357
358 pub fn tick(&mut self, dt: f32) -> Vec<LobbyId> {
360 let mut started = Vec::new();
361 for (id, lobby) in self.lobbies.iter_mut() {
362 if lobby.tick_countdown(dt) {
363 lobby.state = LobbyState::InGame;
364 started.push(*id);
365 }
366 }
367 started
368 }
369
370 pub fn end_game(&mut self, lobby_id: LobbyId) -> Result<(), LobbyError> {
372 let lobby = self.lobbies.get_mut(&lobby_id)
373 .ok_or(LobbyError::LobbyNotFound(lobby_id))?;
374 lobby.state = LobbyState::Postgame;
375 Ok(())
376 }
377
378 pub fn reset_lobby(&mut self, lobby_id: LobbyId) -> Result<(), LobbyError> {
380 let lobby = self.lobbies.get_mut(&lobby_id)
381 .ok_or(LobbyError::LobbyNotFound(lobby_id))?;
382 lobby.state = LobbyState::Waiting;
383 for p in lobby.players.iter_mut() {
384 p.ready = false;
385 }
386 Ok(())
387 }
388
389 pub fn lobby(&self, id: LobbyId) -> Option<&Lobby> {
390 self.lobbies.get(&id)
391 }
392
393 pub fn lobby_for_player(&self, player_id: PlayerId) -> Option<&Lobby> {
394 let lid = self.player_lobby.get(&player_id)?;
395 self.lobbies.get(lid)
396 }
397
398 pub fn lobby_count(&self) -> usize { self.lobbies.len() }
399
400 pub fn public_lobbies(&self) -> Vec<&Lobby> {
402 let mut list: Vec<&Lobby> = self.lobbies.values()
403 .filter(|l| l.config.public && l.state.is_joinable())
404 .collect();
405 list.sort_by(|a, b| b.players.len().cmp(&a.players.len()));
406 list
407 }
408}
409
410impl Default for LobbyManager {
411 fn default() -> Self { Self::new() }
412}
413
414#[derive(Debug, Clone)]
418pub struct LobbyInfo {
419 pub id: LobbyId,
420 pub name: String,
421 pub player_count: u8,
422 pub max_players: u8,
423 pub map_id: u32,
424 pub game_mode: GameMode,
425 pub ping_ms: u32,
427 pub has_password: bool,
428}
429
430impl LobbyInfo {
431 pub fn from_lobby(lobby: &Lobby, ping_ms: u32) -> Self {
432 Self {
433 id: lobby.id,
434 name: lobby.name.clone(),
435 player_count: lobby.players.len() as u8,
436 max_players: lobby.config.max_players,
437 map_id: lobby.config.map_id,
438 game_mode: lobby.config.game_mode,
439 ping_ms,
440 has_password: lobby.config.has_password(),
441 }
442 }
443}
444
445pub struct LobbyBrowser {
449 listings: Vec<LobbyInfo>,
450 last_refresh: Option<Instant>,
451 refresh_cooldown: Duration,
453}
454
455#[derive(Debug, Clone, Copy, PartialEq, Eq)]
457pub enum LobbySort {
458 ByPlayerCount,
459 ByPing,
460 ByName,
461 ByGameMode,
462}
463
464#[derive(Debug, Clone, Default)]
466pub struct LobbyFilter {
467 pub max_ping_ms: Option<u32>,
468 pub game_mode: Option<GameMode>,
469 pub map_id: Option<u32>,
470 pub name_substr: Option<String>,
471 pub hide_full: bool,
472 pub hide_private: bool,
473}
474
475impl LobbyBrowser {
476 pub fn new(refresh_cooldown_ms: u64) -> Self {
477 Self {
478 listings: Vec::new(),
479 last_refresh: None,
480 refresh_cooldown: Duration::from_millis(refresh_cooldown_ms),
481 }
482 }
483
484 pub fn can_refresh(&self) -> bool {
486 match self.last_refresh {
487 None => true,
488 Some(t) => t.elapsed() >= self.refresh_cooldown,
489 }
490 }
491
492 pub fn update_listings(&mut self, infos: Vec<LobbyInfo>) {
494 self.listings = infos;
495 self.last_refresh = Some(Instant::now());
496 }
497
498 pub fn filtered_sorted(&self, filter: &LobbyFilter, sort: LobbySort) -> Vec<&LobbyInfo> {
500 let mut list: Vec<&LobbyInfo> = self.listings.iter()
501 .filter(|l| {
502 if let Some(max_ping) = filter.max_ping_ms {
503 if l.ping_ms > max_ping { return false; }
504 }
505 if let Some(gm) = filter.game_mode {
506 if l.game_mode != gm { return false; }
507 }
508 if let Some(map) = filter.map_id {
509 if l.map_id != map { return false; }
510 }
511 if let Some(ref substr) = filter.name_substr {
512 if !l.name.to_lowercase().contains(&substr.to_lowercase()) { return false; }
513 }
514 if filter.hide_full && l.player_count >= l.max_players { return false; }
515 if filter.hide_private && l.has_password { return false; }
516 true
517 })
518 .collect();
519
520 match sort {
521 LobbySort::ByPlayerCount => list.sort_by(|a, b| b.player_count.cmp(&a.player_count)),
522 LobbySort::ByPing => list.sort_by_key(|l| l.ping_ms),
523 LobbySort::ByName => list.sort_by(|a, b| a.name.cmp(&b.name)),
524 LobbySort::ByGameMode => list.sort_by_key(|l| l.game_mode.0),
525 }
526 list
527 }
528
529 pub fn listing_count(&self) -> usize { self.listings.len() }
530}
531
532#[derive(Debug, Clone)]
536pub struct QueueEntry {
537 pub player_id: PlayerId,
538 pub skill_rating: f32,
539 pub queue_time: Instant,
540 pub party_id: Option<u64>,
542 pub preferences: MatchPreferences,
543}
544
545#[derive(Debug, Clone, Default)]
547pub struct MatchPreferences {
548 pub game_mode: Option<GameMode>,
549 pub preferred_map: Option<u32>,
550 pub max_ping_ms: Option<u32>,
551 pub ranked: bool,
552}
553
554#[derive(Debug, Clone)]
556pub struct ProposedMatch {
557 pub players: Vec<PlayerId>,
558 pub game_mode: GameMode,
559 pub map_id: u32,
560 pub avg_skill: f32,
561}
562
563#[derive(Debug, Clone)]
565pub struct BackfillJob {
566 pub lobby_id: LobbyId,
567 pub open_slots: u8,
568 pub skill_avg: f32,
569 pub game_mode: GameMode,
570 pub map_id: u32,
571}
572
573pub struct MatchmakingQueue {
574 queue: VecDeque<QueueEntry>,
575 backfill_jobs: Vec<BackfillJob>,
576 base_skill_range: f32,
578 range_per_sec: f32,
580 max_skill_range: f32,
582 match_size: usize,
584}
585
586impl MatchmakingQueue {
587 pub fn new(match_size: usize) -> Self {
588 Self {
589 queue: VecDeque::new(),
590 backfill_jobs: Vec::new(),
591 base_skill_range: 50.0,
592 range_per_sec: (500.0 - 50.0) / 60.0, max_skill_range: 500.0,
594 match_size,
595 }
596 }
597
598 pub fn enqueue(&mut self, entry: QueueEntry) {
600 self.queue.push_back(entry);
601 }
602
603 pub fn dequeue(&mut self, player_id: PlayerId) -> bool {
605 let before = self.queue.len();
606 self.queue.retain(|e| e.player_id != player_id);
607 self.queue.len() < before
608 }
609
610 fn skill_range_for(&self, entry: &QueueEntry) -> f32 {
612 let wait_secs = entry.queue_time.elapsed().as_secs_f32();
613 (self.base_skill_range + self.range_per_sec * wait_secs).min(self.max_skill_range)
614 }
615
616 pub fn tick(&mut self) -> Vec<ProposedMatch> {
619 let mut matched: Vec<ProposedMatch> = Vec::new();
620 let mut used: std::collections::HashSet<usize> = std::collections::HashSet::new();
621
622 let entries: Vec<(usize, &QueueEntry)> = self.queue.iter().enumerate().collect();
623
624 'outer: for (i, anchor) in &entries {
625 if used.contains(i) { continue; }
626
627 let range = self.skill_range_for(anchor);
628 let mut group: Vec<usize> = vec![*i];
629 let anchor_party = anchor.party_id;
630
631 for (j, candidate) in &entries {
632 if used.contains(j) || j == i { continue; }
633 if (candidate.skill_rating - anchor.skill_rating).abs() > range { continue; }
634 if let Some(ap) = anchor_party {
636 if candidate.party_id != Some(ap) && group.len() > 1 { continue; }
637 }
638 if let Some(gm) = anchor.preferences.game_mode {
640 if candidate.preferences.game_mode.map_or(false, |cg| cg != gm) { continue; }
641 }
642 group.push(*j);
643 if group.len() >= self.match_size {
644 break;
645 }
646 }
647
648 if group.len() >= self.match_size {
649 let players: Vec<PlayerId> = group.iter()
650 .map(|&idx| entries[idx].1.player_id)
651 .collect();
652 let avg_skill = group.iter().map(|&idx| entries[idx].1.skill_rating).sum::<f32>()
653 / group.len() as f32;
654 let game_mode = entries[group[0]].1.preferences.game_mode
655 .unwrap_or(GameMode::DEATHMATCH);
656
657 for &idx in &group { used.insert(idx); }
658
659 matched.push(ProposedMatch {
660 players,
661 game_mode,
662 map_id: 0,
663 avg_skill,
664 });
665 }
666 }
667
668 let matched_ids: std::collections::HashSet<PlayerId> = matched.iter()
670 .flat_map(|m| m.players.iter().copied())
671 .collect();
672 self.queue.retain(|e| !matched_ids.contains(&e.player_id));
673
674 matched
675 }
676
677 pub fn add_backfill(&mut self, job: BackfillJob) {
679 self.backfill_jobs.push(job);
680 }
681
682 pub fn process_backfill(&mut self) -> Vec<(BackfillJob, Vec<PlayerId>)> {
685 let mut results = Vec::new();
686 let mut filled_ids: std::collections::HashSet<PlayerId> = std::collections::HashSet::new();
687
688 let jobs = std::mem::take(&mut self.backfill_jobs);
689 for job in jobs {
690 let mut candidates: Vec<PlayerId> = Vec::new();
691 for entry in self.queue.iter() {
692 if filled_ids.contains(&entry.player_id) { continue; }
693 if entry.preferences.game_mode.map_or(false, |gm| gm != job.game_mode) { continue; }
694 if (entry.skill_rating - job.skill_avg).abs() > 200.0 { continue; }
695 candidates.push(entry.player_id);
696 if candidates.len() >= job.open_slots as usize { break; }
697 }
698 if !candidates.is_empty() {
699 for &pid in &candidates { filled_ids.insert(pid); }
700 results.push((job, candidates));
701 } else {
702 self.backfill_jobs.push(job); }
704 }
705 self.queue.retain(|e| !filled_ids.contains(&e.player_id));
706 results
707 }
708
709 pub fn queue_len(&self) -> usize { self.queue.len() }
710 pub fn backfill_count(&self) -> usize { self.backfill_jobs.len() }
711}
712
713#[derive(Debug, Clone, Copy, PartialEq, Eq)]
717pub enum VoiceChannel {
718 Lobby,
720 Team(TeamId),
722 Direct(PlayerId),
724}
725
726#[derive(Debug, Clone)]
728pub struct VoiceState {
729 pub player_id: PlayerId,
730 pub channel: VoiceChannel,
731 pub muted: bool,
732 pub deafened: bool,
733 pub push_to_talk: bool,
734 pub speaking: bool,
735}
736
737impl VoiceState {
738 pub fn new(player_id: PlayerId) -> Self {
739 Self {
740 player_id, channel: VoiceChannel::Lobby,
741 muted: false, deafened: false, push_to_talk: false, speaking: false,
742 }
743 }
744}
745
746pub struct VoiceChatManager {
748 states: HashMap<PlayerId, VoiceState>,
749}
750
751impl VoiceChatManager {
752 pub fn new() -> Self { Self { states: HashMap::new() } }
753
754 pub fn add_player(&mut self, player_id: PlayerId) {
755 self.states.insert(player_id, VoiceState::new(player_id));
756 }
757
758 pub fn remove_player(&mut self, player_id: PlayerId) {
759 self.states.remove(&player_id);
760 }
761
762 pub fn set_muted(&mut self, player_id: PlayerId, muted: bool) {
763 if let Some(s) = self.states.get_mut(&player_id) { s.muted = muted; }
764 }
765
766 pub fn set_deafened(&mut self, player_id: PlayerId, deafened: bool) {
767 if let Some(s) = self.states.get_mut(&player_id) { s.deafened = deafened; }
768 }
769
770 pub fn set_push_to_talk(&mut self, player_id: PlayerId, ptt: bool) {
771 if let Some(s) = self.states.get_mut(&player_id) { s.push_to_talk = ptt; }
772 }
773
774 pub fn set_speaking(&mut self, player_id: PlayerId, speaking: bool) {
775 if let Some(s) = self.states.get_mut(&player_id) {
776 if !s.muted { s.speaking = speaking; }
777 }
778 }
779
780 pub fn set_channel(&mut self, player_id: PlayerId, channel: VoiceChannel) {
781 if let Some(s) = self.states.get_mut(&player_id) { s.channel = channel; }
782 }
783
784 pub fn audible_speakers(&self, listener: PlayerId) -> Vec<PlayerId> {
786 let listener_state = match self.states.get(&listener) {
787 Some(s) => s,
788 None => return Vec::new(),
789 };
790 if listener_state.deafened { return Vec::new(); }
791
792 self.states.values()
793 .filter(|s| s.player_id != listener && s.speaking && !s.muted)
794 .filter(|s| {
795 match &s.channel {
797 VoiceChannel::Lobby => true,
798 VoiceChannel::Team(t) => {
799 listener_state.channel == VoiceChannel::Team(*t)
800 }
801 VoiceChannel::Direct(target) => *target == listener,
802 }
803 })
804 .map(|s| s.player_id)
805 .collect()
806 }
807}
808
809impl Default for VoiceChatManager {
810 fn default() -> Self { Self::new() }
811}
812
813#[derive(Debug, Clone, Copy, PartialEq, Eq)]
817pub enum TeamBalance {
818 Auto,
820 Random,
822 Manual,
824}
825
826#[derive(Debug, Clone)]
828pub struct Team {
829 pub id: TeamId,
830 pub name: String,
831 pub players: Vec<PlayerId>,
832 pub max_size: u8,
833}
834
835impl Team {
836 pub fn new(id: TeamId, name: impl Into<String>, max_size: u8) -> Self {
837 Self { id, name: name.into(), players: Vec::new(), max_size }
838 }
839 pub fn is_full(&self) -> bool { self.players.len() >= self.max_size as usize }
840 pub fn has_player(&self, pid: PlayerId) -> bool { self.players.contains(&pid) }
841}
842
843pub struct TeamSystem {
845 pub teams: Vec<Team>,
846 pub balance_mode: TeamBalance,
847 rng_state: u64, }
849
850impl TeamSystem {
851 pub fn new(balance_mode: TeamBalance) -> Self {
852 Self { teams: Vec::new(), balance_mode, rng_state: 0xDEAD_BEEF_CAFE_BABE }
853 }
854
855 fn lcg_rand(&mut self) -> u64 {
856 self.rng_state = self.rng_state.wrapping_mul(6364136223846793005)
857 .wrapping_add(1442695040888963407);
858 self.rng_state
859 }
860
861 pub fn add_team(&mut self, id: TeamId, name: impl Into<String>, max_size: u8) {
862 self.teams.push(Team::new(id, name, max_size));
863 }
864
865 pub fn remove_team(&mut self, id: TeamId) {
866 self.teams.retain(|t| t.id != id);
867 }
868
869 pub fn team(&self, id: TeamId) -> Option<&Team> {
870 self.teams.iter().find(|t| t.id == id)
871 }
872
873 pub fn team_mut(&mut self, id: TeamId) -> Option<&mut Team> {
874 self.teams.iter_mut().find(|t| t.id == id)
875 }
876
877 pub fn assign(
880 &mut self,
881 player_id: PlayerId,
882 skill_rating: f32,
883 preferred_team: Option<TeamId>,
884 ) -> Result<TeamId, LobbyError> {
885 if self.teams.is_empty() {
886 return Err(LobbyError::TeamNotFound(TeamId(0)));
887 }
888
889 match self.balance_mode {
890 TeamBalance::Manual => {
891 let tid = preferred_team.ok_or(LobbyError::TeamNotFound(TeamId(0)))?;
892 let team = self.team_mut(tid).ok_or(LobbyError::TeamNotFound(tid))?;
893 if team.is_full() { return Err(LobbyError::LobbyFull); }
894 team.players.push(player_id);
895 Ok(tid)
896 }
897 TeamBalance::Random => {
898 let n = self.teams.len();
899 let idx = (self.lcg_rand() as usize) % n;
900 for offset in 0..n {
902 let team = &mut self.teams[(idx + offset) % n];
903 if !team.is_full() {
904 team.players.push(player_id);
905 return Ok(team.id);
906 }
907 }
908 Err(LobbyError::LobbyFull)
909 }
910 TeamBalance::Auto => {
911 let mut best_idx = None;
913 let mut best_skill = f32::MAX;
914
915 let _ = skill_rating; for (i, team) in self.teams.iter().enumerate() {
920 if team.is_full() { continue; }
921 let proxy = team.players.len() as f32;
923 if proxy < best_skill {
924 best_skill = proxy;
925 best_idx = Some(i);
926 }
927 }
928 if let Some(i) = best_idx {
929 let tid = self.teams[i].id;
930 self.teams[i].players.push(player_id);
931 Ok(tid)
932 } else {
933 Err(LobbyError::LobbyFull)
934 }
935 }
936 }
937 }
938
939 pub fn remove_player(&mut self, player_id: PlayerId) {
941 for team in &mut self.teams {
942 team.players.retain(|&p| p != player_id);
943 }
944 }
945
946 pub fn rebalance(&mut self) -> Option<(PlayerId, TeamId, TeamId)> {
949 if self.teams.len() < 2 { return None; }
950
951 let max_idx = self.teams.iter().enumerate().max_by_key(|(_, t)| t.players.len())?.0;
952 let min_idx = self.teams.iter().enumerate().min_by_key(|(_, t)| t.players.len())?.0;
953
954 if self.teams[max_idx].players.len() <= self.teams[min_idx].players.len() + 1 {
955 return None; }
957
958 let player = *self.teams[max_idx].players.last()?;
959 self.teams[max_idx].players.pop();
960 let to = self.teams[min_idx].id;
961 let from = self.teams[max_idx].id;
962 self.teams[min_idx].players.push(player);
963 Some((player, from, to))
964 }
965}
966
967#[derive(Debug, Clone, PartialEq, Eq)]
971pub enum ReadyCheckState {
972 Pending,
973 AllReady,
974 TimedOut,
975 Cancelled,
976}
977
978pub struct ReadyCheck {
979 pub votes: HashMap<PlayerId, bool>,
980 pub state: ReadyCheckState,
981 started_at: Instant,
982 timeout_secs: f32,
983}
984
985impl ReadyCheck {
986 pub fn new(timeout_secs: f32) -> Self {
987 Self {
988 votes: HashMap::new(),
989 state: ReadyCheckState::Pending,
990 started_at: Instant::now(),
991 timeout_secs,
992 }
993 }
994
995 pub fn add_participant(&mut self, player_id: PlayerId) {
997 self.votes.insert(player_id, false);
998 }
999
1000 pub fn vote(&mut self, player_id: PlayerId, ready: bool) {
1002 if let Some(v) = self.votes.get_mut(&player_id) {
1003 *v = ready;
1004 }
1005 }
1006
1007 pub fn check(&mut self) -> ReadyCheckState {
1009 if self.state != ReadyCheckState::Pending {
1010 return self.state.clone();
1011 }
1012 if self.started_at.elapsed().as_secs_f32() >= self.timeout_secs {
1013 self.state = ReadyCheckState::TimedOut;
1014 return self.state.clone();
1015 }
1016 if self.votes.values().all(|&v| v) && !self.votes.is_empty() {
1017 self.state = ReadyCheckState::AllReady;
1018 }
1019 self.state.clone()
1020 }
1021
1022 pub fn cancel(&mut self) { self.state = ReadyCheckState::Cancelled; }
1023
1024 pub fn ready_count(&self) -> usize { self.votes.values().filter(|&&v| v).count() }
1025 pub fn total_count(&self) -> usize { self.votes.len() }
1026}
1027
1028#[cfg(test)]
1031mod tests {
1032 use super::*;
1033
1034 fn pid(n: u64) -> PlayerId { PlayerId(n) }
1035 fn lid(n: u64) -> LobbyId { LobbyId(n) }
1036
1037 fn default_config() -> LobbyConfig {
1038 LobbyConfig {
1039 max_players: 4,
1040 min_players: 2,
1041 countdown_secs: 0.01, ..LobbyConfig::default()
1043 }
1044 }
1045
1046 #[test]
1049 fn test_create_and_destroy_lobby() {
1050 let mut mgr = LobbyManager::new();
1051 let lid = mgr.create_lobby(pid(1), "Alice", "Alice's lobby", default_config()).unwrap();
1052 assert_eq!(mgr.lobby_count(), 1);
1053 mgr.destroy_lobby(lid).unwrap();
1054 assert_eq!(mgr.lobby_count(), 0);
1055 }
1056
1057 #[test]
1058 fn test_join_leave() {
1059 let mut mgr = LobbyManager::new();
1060 let lid = mgr.create_lobby(pid(1), "Host", "Lobby", default_config()).unwrap();
1061 mgr.join(pid(2), "P2", lid, "").unwrap();
1062 assert_eq!(mgr.lobby(lid).unwrap().player_count(), 2);
1063 mgr.leave(pid(2)).unwrap();
1064 assert_eq!(mgr.lobby(lid).unwrap().player_count(), 1);
1065 }
1066
1067 #[test]
1068 fn test_kick() {
1069 let mut mgr = LobbyManager::new();
1070 let lid = mgr.create_lobby(pid(1), "Host", "Lobby", default_config()).unwrap();
1071 mgr.join(pid(2), "P2", lid, "").unwrap();
1072 mgr.kick(pid(1), pid(2)).unwrap();
1073 assert_eq!(mgr.lobby(lid).unwrap().player_count(), 1);
1074 }
1075
1076 #[test]
1077 fn test_start_game_requires_min_players() {
1078 let mut mgr = LobbyManager::new();
1079 let lid = mgr.create_lobby(pid(1), "Host", "Lobby", default_config()).unwrap();
1080 assert_eq!(mgr.start_game(pid(1)), Err(LobbyError::NotEnoughPlayers));
1082 mgr.join(pid(2), "P2", lid, "").unwrap();
1083 assert!(mgr.start_game(pid(1)).is_ok());
1084 }
1085
1086 #[test]
1087 fn test_lobby_password() {
1088 let mut mgr = LobbyManager::new();
1089 let config = LobbyConfig { password: "secret".into(), ..default_config() };
1090 let lid = mgr.create_lobby(pid(1), "Host", "Lobby", config).unwrap();
1091 assert_eq!(mgr.join(pid(2), "P2", lid, "wrong"), Err(LobbyError::WrongPassword));
1092 assert!(mgr.join(pid(2), "P2", lid, "secret").is_ok());
1093 }
1094
1095 #[test]
1096 fn test_lobby_full() {
1097 let mut mgr = LobbyManager::new();
1098 let config = LobbyConfig { max_players: 2, ..default_config() };
1099 let lid = mgr.create_lobby(pid(1), "Host", "Lobby", config).unwrap();
1100 mgr.join(pid(2), "P2", lid, "").unwrap();
1101 assert_eq!(mgr.join(pid(3), "P3", lid, ""), Err(LobbyError::LobbyFull));
1102 }
1103
1104 #[test]
1107 fn test_team_auto_balance() {
1108 let mut ts = TeamSystem::new(TeamBalance::Auto);
1109 ts.add_team(TeamId(0), "Red", 4);
1110 ts.add_team(TeamId(1), "Blue", 4);
1111
1112 for i in 0..4u64 {
1113 ts.assign(pid(i), 1000.0, None).unwrap();
1114 }
1115 let r = ts.team(TeamId(0)).unwrap().players.len();
1116 let b = ts.team(TeamId(1)).unwrap().players.len();
1117 assert_eq!(r + b, 4);
1118 assert!((r as i32 - b as i32).abs() <= 1);
1119 }
1120
1121 #[test]
1122 fn test_team_rebalance() {
1123 let mut ts = TeamSystem::new(TeamBalance::Manual);
1124 ts.add_team(TeamId(0), "Red", 8);
1125 ts.add_team(TeamId(1), "Blue", 8);
1126 for p in [0u64, 1, 2] {
1128 ts.team_mut(TeamId(0)).unwrap().players.push(pid(p));
1129 }
1130 ts.team_mut(TeamId(1)).unwrap().players.push(pid(3));
1131 let result = ts.rebalance();
1132 assert!(result.is_some());
1133 let r = ts.team(TeamId(0)).unwrap().players.len();
1134 let b = ts.team(TeamId(1)).unwrap().players.len();
1135 assert_eq!(r, 2);
1136 assert_eq!(b, 2);
1137 }
1138
1139 #[test]
1142 fn test_ready_check_all_ready() {
1143 let mut rc = ReadyCheck::new(30.0);
1144 rc.add_participant(pid(1));
1145 rc.add_participant(pid(2));
1146 rc.vote(pid(1), true);
1147 rc.vote(pid(2), true);
1148 assert_eq!(rc.check(), ReadyCheckState::AllReady);
1149 }
1150
1151 #[test]
1152 fn test_ready_check_not_all_ready() {
1153 let mut rc = ReadyCheck::new(30.0);
1154 rc.add_participant(pid(1));
1155 rc.add_participant(pid(2));
1156 rc.vote(pid(1), true);
1157 assert_eq!(rc.check(), ReadyCheckState::Pending);
1158 }
1159
1160 #[test]
1163 fn test_matchmaking_basic() {
1164 let mut q = MatchmakingQueue::new(2);
1165 for i in 0..2u64 {
1166 q.enqueue(QueueEntry {
1167 player_id: pid(i),
1168 skill_rating: 1000.0,
1169 queue_time: Instant::now(),
1170 party_id: None,
1171 preferences: MatchPreferences::default(),
1172 });
1173 }
1174 let matches = q.tick();
1175 assert_eq!(matches.len(), 1);
1176 assert_eq!(matches[0].players.len(), 2);
1177 }
1178
1179 #[test]
1182 fn test_lobby_browser_filter_and_sort() {
1183 let mut browser = LobbyBrowser::new(1000);
1184 browser.update_listings(vec![
1185 LobbyInfo { id: lid(1), name: "Alpha".into(), player_count: 3, max_players: 8,
1186 map_id: 1, game_mode: GameMode::DEATHMATCH, ping_ms: 50, has_password: false },
1187 LobbyInfo { id: lid(2), name: "Beta".into(), player_count: 1, max_players: 8,
1188 map_id: 2, game_mode: GameMode::TEAM_VS_TEAM, ping_ms: 200, has_password: true },
1189 ]);
1190 let filter = LobbyFilter { hide_private: true, ..LobbyFilter::default() };
1191 let list = browser.filtered_sorted(&filter, LobbySort::ByPlayerCount);
1192 assert_eq!(list.len(), 1);
1193 assert_eq!(list[0].id, lid(1));
1194 }
1195}