1use std::sync::Arc;
2
3use crate::analytics::{self, GameEndReason};
4use crate::error::ServerError;
5use crate::protocol::{
6 DraftConfig, EngineKind, GameFormat, PlayerDeckInfo, ResumeRoomRequest, RoomInfo, RoomStatus,
7 SealedConfig,
8};
9use crate::replay::GameReplayCache;
10use crate::room::{Room, RoomSlot};
11use crate::state::ServerState;
12use manabrew_protocol::deck_dto::Deck;
13use manabrew_protocol::game::PlaymatSettings;
14use manabrew_relay_protocol::{DEFAULT_RECONNECT_TIMEOUT_S, PROTOCOL_VERSION};
15
16const MIN_RECONNECT_TIMEOUT_S: u32 = 10;
17const MAX_RECONNECT_TIMEOUT_S: u32 = 90;
19
20#[allow(clippy::too_many_arguments)]
21pub fn create_room_sync(
22 state: &Arc<ServerState>,
23 player_id: &str,
24 room_name: String,
25 max_players: u8,
26 format: GameFormat,
27 protocol_version: u32,
28 hosted: bool,
29 engine: EngineKind,
30 draft_config: Option<DraftConfig>,
31 sealed_config: Option<SealedConfig>,
32 official_key: Option<String>,
33 password: Option<String>,
34 reconnect_timeout_s: Option<u32>,
35 table_style: Option<String>,
36) -> Result<(RoomInfo, String), ServerError> {
37 if let Some(cfg) = &draft_config {
38 if cfg
39 .pick_seconds
40 .is_some_and(|seconds| !(5..=600).contains(&seconds))
41 {
42 return Err(ServerError::InvalidDraftConfig(
43 "pick_seconds must be between 5 and 600".into(),
44 ));
45 }
46 match (cfg.set_code.as_ref(), cfg.cube_id.as_ref()) {
47 (Some(_), Some(_)) => {
48 return Err(ServerError::InvalidDraftConfig(
49 "set_code and cube_id are mutually exclusive".into(),
50 ));
51 }
52 (None, None) => {
53 return Err(ServerError::InvalidDraftConfig(
54 "set_code or cube_id required".into(),
55 ));
56 }
57 _ => {}
58 }
59 }
60 if let Some(cfg) = &sealed_config {
61 match (cfg.set_code.as_ref(), cfg.cube_id.as_ref()) {
62 (Some(_), Some(_)) => {
63 return Err(ServerError::InvalidDraftConfig(
64 "sealed set_code and cube_id are mutually exclusive".into(),
65 ));
66 }
67 (None, None) => {
68 return Err(ServerError::InvalidDraftConfig(
69 "sealed set_code or cube_id required".into(),
70 ));
71 }
72 _ => {}
73 }
74 }
75
76 {
77 if let Some(player) = state.players.get(player_id) {
78 if let Some(rid) = &player.room_id {
79 return Err(ServerError::AlreadyInRoom(rid.clone()));
80 }
81 } else {
82 return Err(ServerError::AuthFailed("Player not found".into()));
83 }
84 }
85
86 if state.rooms.len() >= state.max_rooms {
87 return Err(ServerError::RoomFull("Server room limit reached".into()));
88 }
89
90 let username = state
91 .players
92 .get(player_id)
93 .map(|p| p.username.clone())
94 .unwrap_or_default();
95
96 let official = match &state.official_key {
97 Some(key) => official_key.as_deref() == Some(key.as_str()),
98 None => false,
99 };
100
101 let room_id = uuid::Uuid::new_v4().to_string();
102 let room = Room::new(
103 room_id.clone(),
104 room_name,
105 protocol_version,
106 player_id.to_string(),
107 username,
108 max_players,
109 format,
110 engine,
111 !hosted,
112 draft_config,
113 sealed_config,
114 official,
115 password.filter(|value| !value.is_empty()),
116 reconnect_timeout_s
117 .unwrap_or(DEFAULT_RECONNECT_TIMEOUT_S)
118 .clamp(MIN_RECONNECT_TIMEOUT_S, MAX_RECONNECT_TIMEOUT_S),
119 table_style,
120 );
121 let info = room.to_room_info();
122 let resume_token = room.resume_token.clone();
123
124 state.rooms.insert(room_id.clone(), room);
125
126 if let Some(mut player) = state.players.get_mut(player_id) {
127 player.room_id = Some(room_id);
128 }
129
130 Ok((info, resume_token))
131}
132
133pub struct ResumedRoom {
134 pub room_info: RoomInfo,
135 pub awaiting_rejoin: Vec<String>,
136}
137
138pub fn resume_room_sync(
139 state: &Arc<ServerState>,
140 player_id: &str,
141 spec: ResumeRoomRequest,
142) -> Result<ResumedRoom, ServerError> {
143 let (username, current_room_id) = {
144 let player = state
145 .players
146 .get(player_id)
147 .ok_or_else(|| ServerError::AuthFailed("Player not found".into()))?;
148 (player.username.clone(), player.room_id.clone())
149 };
150 if let Some(rid) = ¤t_room_id {
151 if rid != &spec.room_id
152 && !state.rooms.get(rid).is_some_and(|room| {
153 room.parent_limited_room.as_deref() == Some(spec.room_id.as_str())
154 })
155 {
156 return Err(ServerError::AlreadyInRoom(rid.clone()));
157 }
158 }
159
160 if let Some(mut room) = state.rooms.get_mut(&spec.room_id) {
161 if room.parent_limited_room.is_some() || room.resume_token != spec.resume_token {
162 return Err(ServerError::InvalidResumeToken);
163 }
164 if room.is_limited_session() && room.host_username != username {
165 return Err(ServerError::InvalidResumeToken);
166 }
167 if room.is_limited_session() && room.status == RoomStatus::Lobby {
168 room.players = resumed_slots(&spec, player_id, &username);
169 room.status = RoomStatus::InGame;
170 room.replay = Some(GameReplayCache::new(
171 spec.game_id.clone(),
172 spec.player_order.clone(),
173 spec.player_decks.clone(),
174 spec.starting_life,
175 ));
176 }
177
178 let old_host_pid = room.host_player_id.clone();
179 room.host_player_id = player_id.to_string();
180 room.host_username = username.clone();
181 if room.hosted {
182 room.remove_observer(&old_host_pid);
183 let _ = room.add_observer(player_id.to_string(), username.clone());
184 room.set_connected(player_id, true);
185 } else if let Some(slot) = room
186 .players
187 .iter_mut()
188 .find(|slot| slot.username == username)
189 {
190 slot.player_id = player_id.to_string();
191 slot.connected = true;
192 }
193 let resumed = ResumedRoom {
194 room_info: room.to_room_info(),
195 awaiting_rejoin: awaiting_rejoin(&room),
196 };
197 drop(room);
198
199 if old_host_pid != player_id {
200 state.players.remove(&old_host_pid);
201 }
202 let current_room_id = state
203 .players
204 .get(player_id)
205 .and_then(|player| player.room_id.clone());
206 let attached_child = current_room_id.as_ref().is_some_and(|id| {
207 state.rooms.get(id).is_some_and(|room| {
208 room.parent_limited_room.as_deref() == Some(spec.room_id.as_str())
209 })
210 });
211 if let Some(mut player) = state.players.get_mut(player_id) {
212 if !attached_child && player.room_id == current_room_id {
213 player.room_id = Some(spec.room_id);
214 }
215 }
216 return Ok(resumed);
217 }
218
219 if state.rooms.len() >= state.max_rooms {
225 return Err(ServerError::RoomFull("Server room limit reached".into()));
226 }
227
228 let official = match &state.official_key {
229 Some(key) => spec.official_key.as_deref() == Some(key.as_str()),
230 None => false,
231 };
232
233 let restored_players = resumed_slots(&spec, player_id, &username);
234 let mut room = Room::new(
235 spec.room_id.clone(),
236 spec.room_name,
237 PROTOCOL_VERSION,
238 player_id.to_string(),
239 username.clone(),
240 spec.max_players,
241 spec.format,
242 spec.engine,
243 !spec.hosted,
244 spec.draft_config,
245 spec.sealed_config,
246 official,
247 spec.password.filter(|value| !value.is_empty()),
248 spec.reconnect_timeout_s
249 .unwrap_or(DEFAULT_RECONNECT_TIMEOUT_S)
250 .clamp(MIN_RECONNECT_TIMEOUT_S, MAX_RECONNECT_TIMEOUT_S),
251 spec.table_style,
252 );
253 room.resume_token = spec.resume_token;
254 room.status = RoomStatus::InGame;
255 room.players = restored_players;
256 room.replay = Some(GameReplayCache::new(
257 spec.game_id,
258 spec.player_order,
259 spec.player_decks,
260 spec.starting_life,
261 ));
262
263 let resumed = ResumedRoom {
264 room_info: room.to_room_info(),
265 awaiting_rejoin: awaiting_rejoin(&room),
266 };
267 let pending_seats: Vec<String> = room
268 .players
269 .iter()
270 .filter(|slot| !slot.connected)
271 .map(|slot| slot.player_id.clone())
272 .collect();
273
274 state.rooms.insert(spec.room_id.clone(), room);
275 for seat_player_id in pending_seats {
276 crate::cleanup::schedule_seat_forfeit(state.clone(), spec.room_id.clone(), seat_player_id);
277 }
278 if let Some(mut player) = state.players.get_mut(player_id) {
279 player.room_id = Some(spec.room_id);
280 }
281
282 Ok(resumed)
283}
284
285fn resumed_slots(spec: &ResumeRoomRequest, player_id: &str, username: &str) -> Vec<RoomSlot> {
286 spec.player_order
287 .iter()
288 .map(|seat_username| {
289 let deck = spec
290 .player_decks
291 .iter()
292 .find(|deck| &deck.username == seat_username);
293 let is_bot = spec.bot_players.contains(seat_username);
294 let is_requester = !spec.hosted && seat_username == username;
295 RoomSlot {
296 player_id: if is_requester {
297 player_id.to_string()
298 } else {
299 format!("pending-rejoin-{}", uuid::Uuid::new_v4())
300 },
301 username: seat_username.clone(),
302 ready: true,
303 connected: is_requester,
304 is_bot,
305 selected_deck_name: deck.map(|d| d.deck_name.clone()),
306 selected_deck: deck.map(|d| d.deck.clone()),
307 published_deck_id: deck.and_then(|d| d.published_deck_id.clone()),
308 selected_commander_name: deck.and_then(|d| d.commander_name.clone()),
309 avatar_url: deck.and_then(|d| d.avatar_url.clone()),
310 }
311 })
312 .collect()
313}
314
315fn awaiting_rejoin(room: &Room) -> Vec<String> {
316 room.players
317 .iter()
318 .filter(|slot| !slot.connected && !slot.is_bot)
319 .map(|slot| slot.username.clone())
320 .collect()
321}
322
323pub fn join_room_sync(
324 state: &Arc<ServerState>,
325 player_id: &str,
326 room_id: &str,
327 observe: bool,
328 as_bot: bool,
329 password: Option<String>,
330) -> Result<(RoomInfo, bool), ServerError> {
331 {
332 if let Some(player) = state.players.get(player_id) {
333 if let Some(rid) = &player.room_id {
334 return Err(ServerError::AlreadyInRoom(rid.clone()));
335 }
336 } else {
337 return Err(ServerError::AuthFailed("Player not found".into()));
338 }
339 }
340
341 let username = state
342 .players
343 .get(player_id)
344 .map(|p| p.username.clone())
345 .unwrap_or_default();
346
347 let (info, rejoined) = {
348 let mut room = state
349 .rooms
350 .get_mut(room_id)
351 .ok_or_else(|| ServerError::RoomNotFound(room_id.to_string()))?;
352
353 if let Some(required) = &room.password {
354 if password.as_deref() != Some(required.as_str()) {
355 return Err(ServerError::IncorrectPassword);
356 }
357 }
358
359 if room.status != RoomStatus::Lobby {
360 if observe {
361 return Err(ServerError::GameAlreadyStarted);
362 }
363 let slot = room
364 .players
365 .iter_mut()
366 .find(|slot| slot.username == username && slot.is_bot == as_bot && !slot.connected)
367 .ok_or(ServerError::GameAlreadyStarted)?;
368 slot.player_id = player_id.to_string();
369 slot.connected = true;
370 (room.to_room_info(), true)
371 } else {
372 if observe {
373 room.add_observer(player_id.to_string(), username)
374 .map_err(|_| ServerError::AlreadyInRoom(room_id.to_string()))?;
375 } else {
376 room.add_player(player_id.to_string(), username, as_bot)
377 .map_err(|msg| {
378 if msg.contains("full") {
379 ServerError::RoomFull(room_id.to_string())
380 } else {
381 ServerError::AlreadyInRoom(room_id.to_string())
382 }
383 })?;
384 }
385 (room.to_room_info(), false)
386 }
387 };
388
389 if let Some(mut player) = state.players.get_mut(player_id) {
390 player.room_id = Some(room_id.to_string());
391 }
392
393 Ok((info, rejoined))
394}
395
396pub fn leave_room_sync(state: &Arc<ServerState>, player_id: &str) -> Result<(), ServerError> {
397 let room_id = {
398 state
399 .players
400 .get(player_id)
401 .and_then(|p| p.room_id.clone())
402 .ok_or(ServerError::NotInRoom)?
403 };
404 let parent_id = state
405 .rooms
406 .get(&room_id)
407 .and_then(|room| room.parent_limited_room.clone());
408 let mut parent_empty = false;
409 if let Some(parent_id) = &parent_id {
410 let info = state.rooms.get_mut(parent_id).map(|mut parent| {
411 parent.remove_participant(player_id);
412 parent_empty = parent.is_empty();
413 parent.to_room_info()
414 });
415 if let Some(info) = info {
416 crate::connection::broadcast_to_room(
417 state,
418 parent_id,
419 &crate::protocol::ServerMessage::RoomUpdate { room: info },
420 );
421 }
422 }
423
424 let (room_empty, no_connected_players) = {
425 let mut room = state
426 .rooms
427 .get_mut(&room_id)
428 .ok_or_else(|| ServerError::RoomNotFound(room_id.clone()))?;
429
430 room.remove_participant(player_id);
431 let empty = room.is_empty();
432 let no_connected = if room.parent_limited_room.is_some() {
433 !crate::cleanup::paired_room_has_participants(state, &room, std::time::Instant::now())
434 } else {
435 room.all_disconnected()
436 };
437 if !empty && !no_connected && room.players.is_empty() {
438 room.reset_lobby_settings();
439 }
440 (empty, no_connected)
441 };
442
443 if room_empty || no_connected_players {
444 crate::cleanup::remove_room_and_clear_sessions(state, &room_id, GameEndReason::Abandoned);
445 }
446
447 if let Some(mut player) = state.players.get_mut(player_id) {
448 player.room_id = None;
449 }
450 if parent_empty {
451 if let Some(parent_id) = parent_id {
452 crate::cleanup::remove_room_and_clear_sessions(
453 state,
454 &parent_id,
455 GameEndReason::Abandoned,
456 );
457 }
458 }
459
460 Ok(())
461}
462
463pub fn set_ready_sync(
464 state: &Arc<ServerState>,
465 player_id: &str,
466 ready: bool,
467) -> Result<String, ServerError> {
468 let room_id = {
469 state
470 .players
471 .get(player_id)
472 .and_then(|p| p.room_id.clone())
473 .ok_or(ServerError::NotInRoom)?
474 };
475
476 {
477 let mut room = state
478 .rooms
479 .get_mut(&room_id)
480 .ok_or_else(|| ServerError::RoomNotFound(room_id.clone()))?;
481
482 if room.status != RoomStatus::Lobby {
483 return Err(ServerError::GameAlreadyStarted);
484 }
485
486 let format_requires_deck = room.format != GameFormat::Any && !room.is_limited_session();
487 if ready && format_requires_deck && !room.has_selected_deck(player_id) {
488 return Err(ServerError::DeckNotSelected);
489 }
490
491 room.set_ready(player_id, ready)
492 .map_err(|_| ServerError::NotInRoom)?;
493 }
494
495 Ok(room_id)
496}
497
498const MAX_COLOR_LEN: usize = 32;
499const MAX_PUBLISHED_DECK_ID_LEN: usize = 160;
500
501fn sanitize_playmat_settings(settings: &mut PlaymatSettings) {
502 settings.color = settings.color.take().filter(|s| s.len() <= MAX_COLOR_LEN);
503 settings.border_color = settings
504 .border_color
505 .take()
506 .filter(|s| s.len() <= MAX_COLOR_LEN);
507}
508
509fn sanitize_published_deck_id(value: Option<String>) -> Option<String> {
510 value.filter(|id| {
511 !id.is_empty()
512 && id.len() <= MAX_PUBLISHED_DECK_ID_LEN
513 && id
514 .bytes()
515 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b':'))
516 })
517}
518
519pub fn set_deck_selection_sync(
520 state: &Arc<ServerState>,
521 player_id: &str,
522 deck_name: String,
523 mut deck: Deck,
524 published_deck_id: Option<String>,
525 commander_name: Option<String>,
526 avatar_url: Option<String>,
527) -> Result<String, ServerError> {
528 let room_id = {
529 state
530 .players
531 .get(player_id)
532 .and_then(|p| p.room_id.clone())
533 .ok_or(ServerError::NotInRoom)?
534 };
535
536 if let Some(settings) = deck.playmat_settings.as_mut() {
537 sanitize_playmat_settings(settings);
538 }
539 let published_deck_id = sanitize_published_deck_id(published_deck_id);
540
541 {
542 let mut room = state
543 .rooms
544 .get_mut(&room_id)
545 .ok_or_else(|| ServerError::RoomNotFound(room_id.clone()))?;
546
547 if room.status != RoomStatus::Lobby {
548 return Err(ServerError::GameAlreadyStarted);
549 }
550
551 room.set_deck_selection(
552 player_id,
553 deck_name,
554 deck,
555 published_deck_id,
556 commander_name,
557 avatar_url,
558 )
559 .map_err(|_| ServerError::NotInRoom)?;
560 }
561
562 Ok(room_id)
563}
564
565pub struct StartedGame {
566 pub game_id: String,
567 pub room_id: String,
568 pub player_order: Vec<String>,
569 pub player_decks: Vec<PlayerDeckInfo>,
570 pub starting_life: i32,
571 pub room_info: RoomInfo,
572}
573
574pub fn set_format_sync(
575 state: &Arc<ServerState>,
576 player_id: &str,
577 format: GameFormat,
578) -> Result<String, ServerError> {
579 let room_id = {
580 state
581 .players
582 .get(player_id)
583 .and_then(|p| p.room_id.clone())
584 .ok_or(ServerError::NotInRoom)?
585 };
586
587 {
588 let mut room = state
589 .rooms
590 .get_mut(&room_id)
591 .ok_or_else(|| ServerError::RoomNotFound(room_id.clone()))?;
592
593 if !room.is_controller(player_id) {
594 return Err(ServerError::NotHost);
595 }
596
597 if room.status != RoomStatus::Lobby {
598 return Err(ServerError::GameAlreadyStarted);
599 }
600
601 if room.format != format {
602 room.format = format;
603 for player in room.players.iter_mut().filter(|p| !p.is_bot) {
604 player.ready = false;
605 }
606 }
607 }
608
609 Ok(room_id)
610}
611
612pub fn set_max_players_sync(
613 state: &Arc<ServerState>,
614 player_id: &str,
615 max_players: u8,
616) -> Result<String, ServerError> {
617 let room_id = {
618 state
619 .players
620 .get(player_id)
621 .and_then(|p| p.room_id.clone())
622 .ok_or(ServerError::NotInRoom)?
623 };
624
625 {
626 let mut room = state
627 .rooms
628 .get_mut(&room_id)
629 .ok_or_else(|| ServerError::RoomNotFound(room_id.clone()))?;
630
631 if !room.is_controller(player_id) {
632 return Err(ServerError::NotHost);
633 }
634
635 if room.status != RoomStatus::Lobby {
636 return Err(ServerError::GameAlreadyStarted);
637 }
638
639 let floor = (room.players.len() as u8).max(2);
640 room.max_players = max_players.clamp(floor, 4);
641 }
642
643 Ok(room_id)
644}
645
646pub fn start_game_sync(
647 state: &Arc<ServerState>,
648 player_id: &str,
649 format: Option<GameFormat>,
650) -> Result<StartedGame, ServerError> {
651 let room_id = {
652 state
653 .players
654 .get(player_id)
655 .and_then(|p| p.room_id.clone())
656 .ok_or(ServerError::NotInRoom)?
657 };
658
659 let (game_id, player_order, player_decks, starting_life, room_info) = {
660 let mut room = state
661 .rooms
662 .get_mut(&room_id)
663 .ok_or_else(|| ServerError::RoomNotFound(room_id.clone()))?;
664
665 if !room.is_controller(player_id) {
666 return Err(ServerError::NotHost);
667 }
668
669 if room.status != RoomStatus::Lobby {
670 return Err(ServerError::GameAlreadyStarted);
671 }
672
673 if room.format == GameFormat::Any {
674 match format {
675 Some(chosen) if chosen != GameFormat::Any => room.format = chosen,
676 _ => return Err(ServerError::FormatNotChosen),
677 }
678 }
679
680 if !room.all_ready() {
681 return Err(ServerError::PlayersNotReady);
682 }
683
684 if !room.is_limited_session()
685 && room
686 .players
687 .iter()
688 .any(|p| p.selected_deck_name.is_none() || p.selected_deck.is_none())
689 {
690 return Err(ServerError::DeckNotSelected);
691 }
692
693 room.status = RoomStatus::InGame;
694 let starting_life = match room.format {
695 GameFormat::Commander => 40,
696 GameFormat::Brawl => 25,
697 GameFormat::PauperCommander => 30,
698 GameFormat::Standard
699 | GameFormat::Pioneer
700 | GameFormat::Modern
701 | GameFormat::Legacy
702 | GameFormat::Vintage
703 | GameFormat::Pauper
704 | GameFormat::Premodern
705 | GameFormat::Oathbreaker
706 | GameFormat::Draft
707 | GameFormat::Sealed => 20,
708 GameFormat::Any => unreachable!("Any resolved to concrete format above"),
709 };
710 let player_order = room.player_usernames();
711 let player_decks = room.player_decks();
712 let game_id = uuid::Uuid::new_v4().to_string();
713 room.replay = Some(GameReplayCache::new(
714 game_id.clone(),
715 player_order.clone(),
716 player_decks.clone(),
717 starting_life,
718 ));
719 (
720 game_id,
721 player_order,
722 player_decks,
723 starting_life,
724 room.to_room_info(),
725 )
726 };
727
728 Ok(StartedGame {
729 game_id,
730 room_id,
731 player_order,
732 player_decks,
733 starting_life,
734 room_info,
735 })
736}
737
738pub fn end_game_sync(
739 state: &Arc<ServerState>,
740 player_id: &str,
741 game_id: &str,
742) -> Result<(String, RoomInfo, Vec<String>), ServerError> {
743 let room_id = state
744 .players
745 .get(player_id)
746 .and_then(|p| p.room_id.clone())
747 .ok_or(ServerError::NotInRoom)?;
748
749 {
750 let room = state
751 .rooms
752 .get(&room_id)
753 .ok_or_else(|| ServerError::RoomNotFound(room_id.clone()))?;
754 if !room.is_host(player_id) {
755 return Err(ServerError::NotHost);
756 }
757 if room.status != RoomStatus::InGame {
758 return Err(ServerError::GameNotInProgress);
759 }
760 if room.replay.as_ref().is_some_and(|r| r.game_id != game_id) {
761 return Err(ServerError::GameNotInProgress);
762 }
763 }
764
765 let (info, notify) = reset_room_to_lobby(state, &room_id, GameEndReason::HostEnded)
766 .ok_or(ServerError::RoomNotFound(room_id.clone()))?;
767
768 Ok((room_id, info, notify))
769}
770
771pub fn reset_room_to_lobby(
772 state: &Arc<ServerState>,
773 room_id: &str,
774 reason: GameEndReason,
775) -> Option<(RoomInfo, Vec<String>)> {
776 let (info, cleared, paired_limited, children) = {
777 let mut room = state.rooms.get_mut(room_id)?;
778 if let Some(replay) = room.replay.take() {
779 analytics::emit_game_ended(&state.analytics, &room, &replay, reason);
780 }
781 let cleared: Vec<String> = room.players.iter().map(|p| p.player_id.clone()).collect();
782 room.status = RoomStatus::Lobby;
783 let paired_limited = room.parent_limited_room.is_some();
784 let children = if paired_limited {
785 for seat in &mut room.players {
786 seat.ready = false;
787 }
788 Vec::new()
789 } else {
790 std::mem::take(&mut room.limited_matches)
791 };
792 if !paired_limited {
793 room.players.clear();
794 room.limited_session_id = None;
795 room.draft_clock = None;
796 }
797 if !paired_limited {
798 room.reset_lobby_settings();
799 }
800 (room.to_room_info(), cleared, paired_limited, children)
801 };
802 for child_id in children {
803 let active = state
804 .rooms
805 .get(&child_id)
806 .is_some_and(|child| child.status == RoomStatus::InGame);
807 if active {
808 crate::connection::broadcast_to_room(
809 state,
810 &child_id,
811 &crate::protocol::ServerMessage::GameAborted {
812 room_id: child_id.clone(),
813 },
814 );
815 }
816 crate::cleanup::remove_room_and_clear_sessions(state, &child_id, reason);
817 }
818
819 let mut notify = Vec::new();
820 for pid in cleared {
821 if paired_limited {
822 notify.push(pid);
823 continue;
824 }
825 match state.players.get(&pid).map(|p| p.disconnected_at.is_some()) {
826 Some(true) => {
827 state.players.remove(&pid);
828 }
829 Some(false) => {
830 if let Some(mut p) = state.players.get_mut(&pid) {
831 p.room_id = None;
832 }
833 notify.push(pid);
834 }
835 None => {}
836 }
837 }
838
839 Some((info, notify))
840}