1use std::time::Instant;
2
3use crate::chat::ChatHistory;
4use crate::protocol::{
5 DraftConfig, EngineKind, GameFormat, PlayerDeckInfo, RoomInfo, RoomPlayerInfo, RoomStatus,
6 SealedConfig,
7};
8use crate::replay::GameReplayCache;
9use manabrew_protocol::deck_dto::Deck;
10
11#[derive(Debug, Clone)]
12pub struct RoomSlot {
13 pub player_id: String,
14 pub username: String,
15 pub ready: bool,
16 pub connected: bool,
17 pub is_bot: bool,
18 pub selected_deck_name: Option<String>,
19 pub selected_deck: Option<Deck>,
20 pub published_deck_id: Option<String>,
21 pub selected_commander_name: Option<String>,
22 pub avatar_url: Option<String>,
23}
24
25#[derive(Debug, Clone)]
26pub struct RoomObserver {
27 pub player_id: String,
28 pub connected: bool,
29}
30
31#[derive(Debug)]
32pub struct Room {
33 pub room_id: String,
34 pub room_name: String,
35 pub protocol_version: u32,
36 pub host_player_id: String,
37 pub host_username: String,
38 pub hosted: bool,
39 pub official: bool,
40 pub password: Option<String>,
41 pub max_players: u8,
42 pub format: GameFormat,
43 pub engine: EngineKind,
44 pub status: RoomStatus,
45 pub players: Vec<RoomSlot>,
46 pub observers: Vec<RoomObserver>,
47 pub draft_config: Option<DraftConfig>,
48 pub sealed_config: Option<SealedConfig>,
49 pub reconnect_timeout_s: u32,
50 pub replay: Option<GameReplayCache>,
51 pub resume_token: String,
52 pub humanless_since: Option<Instant>,
53 pub chat: ChatHistory,
54}
55
56impl Room {
57 #[allow(clippy::too_many_arguments)]
58 pub fn new(
59 room_id: String,
60 room_name: String,
61 protocol_version: u32,
62 host_player_id: String,
63 host_username: String,
64 max_players: u8,
65 format: GameFormat,
66 engine: EngineKind,
67 host_plays: bool,
68 draft_config: Option<DraftConfig>,
69 sealed_config: Option<SealedConfig>,
70 official: bool,
71 password: Option<String>,
72 reconnect_timeout_s: u32,
73 ) -> Self {
74 let max_players = max_players.clamp(2, 8);
75 let (players, observers) = if host_plays {
76 (
77 vec![RoomSlot {
78 player_id: host_player_id.clone(),
79 username: host_username.clone(),
80 ready: false,
81 connected: true,
82 is_bot: false,
83 selected_deck_name: None,
84 selected_deck: None,
85 published_deck_id: None,
86 selected_commander_name: None,
87 avatar_url: None,
88 }],
89 vec![],
90 )
91 } else {
92 (
93 vec![],
94 vec![RoomObserver {
95 player_id: host_player_id.clone(),
96 connected: true,
97 }],
98 )
99 };
100 Room {
101 room_id,
102 room_name,
103 protocol_version,
104 host_player_id: host_player_id.clone(),
105 host_username,
106 hosted: !host_plays,
107 official,
108 password,
109 max_players,
110 format,
111 engine,
112 status: RoomStatus::Lobby,
113 players,
114 observers,
115 draft_config,
116 sealed_config,
117 reconnect_timeout_s,
118 replay: None,
119 resume_token: String::new(),
120 humanless_since: None,
121 chat: ChatHistory::default(),
122 }
123 }
124
125 pub fn is_full(&self) -> bool {
126 self.players.len() >= self.max_players as usize
127 }
128
129 pub fn is_empty(&self) -> bool {
130 self.players.is_empty() && self.observers.is_empty()
131 }
132
133 pub fn is_limited_session(&self) -> bool {
134 matches!(
135 self.format,
136 GameFormat::Any | GameFormat::Draft | GameFormat::Sealed
137 ) && (self.draft_config.is_some() || self.sealed_config.is_some())
138 }
139
140 pub fn reset_lobby_settings(&mut self) {
141 if self.draft_config.is_none() && self.sealed_config.is_none() {
142 self.format = GameFormat::Any;
143 self.max_players = 4;
144 }
145 }
146
147 pub fn all_ready(&self) -> bool {
148 let min_players = if self.is_limited_session() { 1 } else { 2 };
149 let controller_id = self.controller_id();
150 self.players.len() >= min_players
151 && self
152 .players
153 .iter()
154 .filter(|p| Some(p.player_id.as_str()) != controller_id)
155 .all(|p| p.ready)
156 }
157
158 pub fn add_player(
159 &mut self,
160 player_id: String,
161 username: String,
162 is_bot: bool,
163 ) -> Result<(), String> {
164 if self.is_full() {
165 return Err("Room is full".into());
166 }
167 if self.status != RoomStatus::Lobby {
168 return Err("Game already started".into());
169 }
170 if self.players.iter().any(|p| p.player_id == player_id) {
171 return Err("Already in this room".into());
172 }
173 self.players.push(RoomSlot {
174 player_id,
175 username,
176 ready: false,
177 connected: true,
178 is_bot,
179 selected_deck_name: None,
180 selected_deck: None,
181 published_deck_id: None,
182 selected_commander_name: None,
183 avatar_url: None,
184 });
185 Ok(())
186 }
187
188 pub fn add_observer(&mut self, player_id: String, _username: String) -> Result<(), String> {
189 if self.players.iter().any(|p| p.player_id == player_id)
190 || self.observers.iter().any(|p| p.player_id == player_id)
191 {
192 return Err("Already in this room".into());
193 }
194 self.observers.push(RoomObserver {
195 player_id,
196 connected: true,
197 });
198 Ok(())
199 }
200
201 pub fn remove_player(&mut self, player_id: &str) -> Option<RoomSlot> {
202 if let Some(idx) = self.players.iter().position(|p| p.player_id == player_id) {
203 let slot = self.players.remove(idx);
204 if self.host_player_id == player_id {
206 if let Some(new_host) = self.players.first() {
207 self.host_player_id = new_host.player_id.clone();
208 self.host_username = new_host.username.clone();
209 }
210 }
211 Some(slot)
212 } else {
213 None
214 }
215 }
216
217 pub fn remove_observer(&mut self, player_id: &str) -> Option<RoomObserver> {
218 self.observers
219 .iter()
220 .position(|p| p.player_id == player_id)
221 .map(|idx| self.observers.remove(idx))
222 }
223
224 pub fn remove_participant(&mut self, player_id: &str) -> bool {
225 self.remove_player(player_id).is_some() || self.remove_observer(player_id).is_some()
226 }
227
228 pub fn set_connected(&mut self, player_id: &str, connected: bool) {
229 if let Some(slot) = self.players.iter_mut().find(|p| p.player_id == player_id) {
230 slot.connected = connected;
231 return;
232 }
233 if let Some(observer) = self.observers.iter_mut().find(|p| p.player_id == player_id) {
234 observer.connected = connected;
235 }
236 }
237
238 #[allow(dead_code)]
240 pub fn all_disconnected(&self) -> bool {
241 self.players.iter().all(|p| !p.connected) && self.observers.iter().all(|p| !p.connected)
242 }
243
244 pub fn has_connected_human(&self) -> bool {
245 self.players.iter().any(|p| p.connected && !p.is_bot)
246 || self
247 .observers
248 .iter()
249 .any(|o| o.connected && !(self.hosted && o.player_id == self.host_player_id))
250 }
251
252 pub fn set_ready(&mut self, player_id: &str, ready: bool) -> Result<(), String> {
253 if let Some(slot) = self.players.iter_mut().find(|p| p.player_id == player_id) {
254 slot.ready = ready;
255 Ok(())
256 } else {
257 Err("Player not in room".into())
258 }
259 }
260
261 pub fn set_deck_selection(
262 &mut self,
263 player_id: &str,
264 deck_name: String,
265 deck: Deck,
266 published_deck_id: Option<String>,
267 commander_name: Option<String>,
268 avatar_url: Option<String>,
269 ) -> Result<(), String> {
270 if let Some(slot) = self.players.iter_mut().find(|p| p.player_id == player_id) {
271 slot.selected_deck_name = Some(deck_name);
272 slot.selected_deck = Some(deck);
273 slot.published_deck_id = published_deck_id;
274 slot.selected_commander_name = commander_name;
275 slot.avatar_url = avatar_url;
276 slot.ready = false;
277 Ok(())
278 } else {
279 Err("Player not in room".into())
280 }
281 }
282
283 pub fn has_selected_deck(&self, player_id: &str) -> bool {
284 self.players
285 .iter()
286 .find(|p| p.player_id == player_id)
287 .map(|p| p.selected_deck.is_some() && p.selected_deck_name.is_some())
288 .unwrap_or(false)
289 }
290
291 pub fn player_decks(&self) -> Vec<PlayerDeckInfo> {
292 self.players
293 .iter()
294 .filter_map(|p| {
295 p.selected_deck.clone().map(|deck| PlayerDeckInfo {
296 username: p.username.clone(),
297 deck_name: p
298 .selected_deck_name
299 .clone()
300 .unwrap_or_else(|| "Unknown Deck".to_string()),
301 deck,
302 published_deck_id: p.published_deck_id.clone(),
303 commander_name: p.selected_commander_name.clone(),
304 avatar_url: p.avatar_url.clone(),
305 })
306 })
307 .collect()
308 }
309
310 pub fn is_host(&self, player_id: &str) -> bool {
311 self.host_player_id == player_id
312 }
313
314 pub fn controller_id(&self) -> Option<&str> {
320 self.players
321 .iter()
322 .find(|p| !p.is_bot)
323 .or_else(|| self.players.first())
324 .map(|p| p.player_id.as_str())
325 }
326
327 pub fn is_controller(&self, player_id: &str) -> bool {
328 self.controller_id() == Some(player_id)
329 }
330
331 pub fn host_username(&self) -> String {
332 self.host_username.clone()
333 }
334
335 pub fn host_is_player(&self) -> bool {
336 self.players
337 .iter()
338 .any(|p| p.player_id == self.host_player_id)
339 }
340
341 pub fn host_connected(&self) -> bool {
342 self.players
343 .iter()
344 .any(|p| p.player_id == self.host_player_id && p.connected)
345 || self
346 .observers
347 .iter()
348 .any(|p| p.player_id == self.host_player_id && p.connected)
349 }
350
351 pub fn player_usernames(&self) -> Vec<String> {
352 self.players.iter().map(|p| p.username.clone()).collect()
353 }
354
355 #[allow(dead_code)]
357 pub fn player_ids(&self) -> Vec<String> {
358 self.players.iter().map(|p| p.player_id.clone()).collect()
359 }
360
361 pub fn connected_player_ids(&self) -> Vec<String> {
362 self.players
363 .iter()
364 .filter(|p| p.connected)
365 .map(|p| p.player_id.clone())
366 .chain(
367 self.observers
368 .iter()
369 .filter(|p| p.connected)
370 .map(|p| p.player_id.clone()),
371 )
372 .collect()
373 }
374
375 pub fn to_room_info(&self) -> RoomInfo {
376 RoomInfo {
377 room_id: self.room_id.clone(),
378 room_name: self.room_name.clone(),
379 protocol_version: self.protocol_version,
380 host: self.host_username(),
381 hosted: self.hosted,
382 official: self.official,
383 password_protected: self.password.is_some(),
384 players: self
385 .players
386 .iter()
387 .map(|p| RoomPlayerInfo {
388 username: p.username.clone(),
389 ready: p.ready,
390 connected: p.connected,
391 is_bot: p.is_bot,
392 selected_deck_name: p.selected_deck_name.clone(),
393 })
394 .collect(),
395 max_players: self.max_players,
396 format: self.format.clone(),
397 engine: self.engine,
398 reconnect_timeout_s: self.reconnect_timeout_s,
399 status: self.status.clone(),
400 draft_config: self.draft_config.clone(),
401 sealed_config: self.sealed_config.clone(),
402 }
403 }
404}
405
406#[cfg(test)]
407mod tests {
408 use super::*;
409
410 fn room(host_plays: bool) -> Room {
411 Room::new(
412 "r".into(),
413 "room".into(),
414 manabrew_relay_protocol::PROTOCOL_VERSION,
415 "host".into(),
416 "host".into(),
417 4,
418 GameFormat::Commander,
419 EngineKind::Forge,
420 host_plays,
421 None,
422 None,
423 false,
424 None,
425 60,
426 )
427 }
428
429 #[test]
430 fn first_user_is_controller_in_hosted_room() {
431 let mut r = room(false);
432 assert!(r.is_host("host"));
433 assert!(!r.is_controller("host"));
434 r.add_player("human".into(), "human".into(), false).unwrap();
435 assert!(r.is_controller("human"));
436 r.add_player("bot".into(), "bot".into(), true).unwrap();
437 assert!(!r.is_controller("bot"));
438 }
439}