1pub use crate::deck_dto::Deck;
2use serde::{Deserialize, Serialize};
3use ts_rs::TS;
4
5pub const PROTOCOL_VERSION: u32 = major_of(env!("CARGO_PKG_VERSION_MAJOR"));
8
9const fn major_of(major: &str) -> u32 {
10 let bytes = major.as_bytes();
11 let mut value = 0u32;
12 let mut i = 0;
13 while i < bytes.len() {
14 value = value * 10 + (bytes[i] - b'0') as u32;
15 i += 1;
16 }
17 value
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize, TS)]
21#[ts(export, export_to = "lobby/index.ts")]
22pub struct PlayerDeckInfo {
23 pub username: String,
24 pub deck_name: String,
25 pub deck: Deck,
26 #[serde(default, skip_serializing_if = "Option::is_none")]
27 #[ts(optional)]
28 pub published_deck_id: Option<String>,
29 #[serde(skip_serializing_if = "Option::is_none")]
30 #[ts(optional)]
31 pub commander_name: Option<String>,
32 #[serde(default, skip_serializing_if = "Option::is_none")]
33 #[ts(optional)]
34 pub avatar: Option<String>,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
38#[serde(tag = "type")]
39#[allow(clippy::large_enum_variant)]
40pub enum ClientMessage {
41 Authenticate {
42 username: String,
43 password: String,
44 #[serde(default)]
45 service: bool,
46 },
47
48 Ping,
49
50 ListRooms,
51
52 ListPlayers,
53
54 CreateRoom {
55 room_name: String,
56 max_players: u8,
57 format: GameFormat,
58 #[serde(default)]
59 protocol_version: u32,
60 #[serde(default)]
61 hosted: bool,
62 #[serde(default)]
63 engine: EngineKind,
64 #[serde(default)]
65 draft_config: Option<DraftConfig>,
66 #[serde(default)]
67 sealed_config: Option<SealedConfig>,
68 #[serde(default)]
69 official_key: Option<String>,
70 #[serde(default)]
71 password: Option<String>,
72 #[serde(default)]
73 reconnect_timeout_s: Option<u32>,
74 },
75
76 JoinRoom {
77 room_id: String,
78 #[serde(default)]
79 observe: bool,
80 #[serde(default)]
81 as_bot: bool,
82 #[serde(default)]
83 password: Option<String>,
84 },
85
86 ResumeRoom(ResumeRoomRequest),
87
88 LeaveRoom,
89
90 SetReady {
91 ready: bool,
92 },
93
94 SetDeckSelection {
95 deck_name: String,
96 deck: Deck,
97 #[serde(default)]
98 published_deck_id: Option<String>,
99 commander_name: Option<String>,
100 #[serde(default)]
101 avatar: Option<String>,
102 },
103
104 SetFormat {
105 format: GameFormat,
106 },
107
108 SetMaxPlayers {
109 max_players: u8,
110 },
111
112 StartGame {
113 #[serde(default)]
114 format: Option<GameFormat>,
115 },
116
117 EndGame {
118 game_id: String,
119 },
120
121 RequestResync,
122
123 BroadcastState {
124 state: serde_json::Value,
125 #[serde(default, skip_serializing_if = "Option::is_none")]
126 target_player: Option<String>,
128 },
129
130 TurnChange {
131 new_active_player: String,
132 turn_number: u32,
133 },
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
137#[serde(tag = "type")]
138pub enum ServerMessage {
139 AuthResult {
140 success: bool,
141 player_id: Option<String>,
142 reconnected: Option<bool>,
143 error: Option<String>,
144 },
145
146 RoomList {
147 rooms: Vec<RoomInfo>,
148 },
149
150 PlayerList {
151 players: Vec<PlayerInfo>,
152 },
153
154 RoomCreated {
155 room_id: String,
156 room_name: String,
157 room: RoomInfo,
158 #[serde(default)]
159 resume_token: Option<String>,
160 },
161
162 RoomResumed {
163 room: RoomInfo,
164 },
165
166 PlayerJoined {
167 room_id: String,
168 username: String,
169 },
170
171 PlayerLeft {
172 room_id: String,
173 username: String,
174 },
175
176 PlayerConnected {
177 username: String,
178 },
179
180 PlayerDisconnected {
181 username: String,
182 },
183
184 ReadyStateChanged {
185 username: String,
186 ready: bool,
187 },
188
189 RoomUpdate {
190 room: RoomInfo,
191 },
192
193 GameStarted {
194 room_id: String,
195 game_id: String,
196 player_order: Vec<String>,
197 player_decks: Vec<PlayerDeckInfo>,
198 starting_life: i32,
199 },
200
201 StateUpdate {
202 from_player: String,
203 state: serde_json::Value,
204 },
205
206 TurnChanged {
207 from_player: String,
208 new_active_player: String,
209 turn_number: u32,
210 },
211
212 GameAborted {
213 room_id: String,
214 },
215
216 Error {
217 code: String,
218 message: String,
219 },
220
221 ServerShuttingDown {
222 reconnect_in_s: u32,
223 },
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize, TS)]
227#[ts(export, export_to = "lobby/index.ts")]
228pub struct ResumeRoomRequest {
229 pub room_id: String,
230 pub resume_token: String,
231 pub room_name: String,
232 pub max_players: u8,
233 pub format: GameFormat,
234 #[serde(default)]
235 pub hosted: bool,
236 #[serde(default)]
237 pub engine: EngineKind,
238 #[serde(default, skip_serializing_if = "Option::is_none")]
239 #[ts(optional)]
240 pub official_key: Option<String>,
241 #[serde(default, skip_serializing_if = "Option::is_none")]
242 #[ts(optional)]
243 pub password: Option<String>,
244 #[serde(default, skip_serializing_if = "Option::is_none")]
245 #[ts(optional)]
246 pub reconnect_timeout_s: Option<u32>,
247 #[serde(default, skip_serializing_if = "Option::is_none")]
248 #[ts(optional)]
249 pub draft_config: Option<DraftConfig>,
250 #[serde(default, skip_serializing_if = "Option::is_none")]
251 #[ts(optional)]
252 pub sealed_config: Option<SealedConfig>,
253 pub player_order: Vec<String>,
254 pub player_decks: Vec<PlayerDeckInfo>,
255 pub starting_life: i32,
256 #[serde(default)]
257 pub bot_players: Vec<String>,
258 pub game_id: String,
259}
260
261#[derive(Debug, Clone, Serialize, Deserialize)]
262pub struct RoomInfo {
263 pub room_id: String,
264 pub room_name: String,
265 pub host: String,
266 #[serde(default)]
267 pub protocol_version: u32,
268 #[serde(default)]
269 pub hosted: bool,
270 #[serde(default)]
271 pub official: bool,
272 #[serde(default)]
273 pub password_protected: bool,
274 pub players: Vec<RoomPlayerInfo>,
275 pub max_players: u8,
276 pub format: GameFormat,
277 pub status: RoomStatus,
278 #[serde(default)]
279 pub engine: EngineKind,
280 #[serde(default = "default_reconnect_timeout_s")]
281 pub reconnect_timeout_s: u32,
282 #[serde(default, skip_serializing_if = "Option::is_none")]
283 pub draft_config: Option<DraftConfig>,
284 #[serde(default, skip_serializing_if = "Option::is_none")]
285 pub sealed_config: Option<SealedConfig>,
286}
287
288pub const DEFAULT_RECONNECT_TIMEOUT_S: u32 = 60;
289
290fn default_reconnect_timeout_s() -> u32 {
291 DEFAULT_RECONNECT_TIMEOUT_S
292}
293
294#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
295#[ts(export, export_to = "lobby/index.ts")]
296pub struct SealedConfig {
297 #[serde(default, skip_serializing_if = "Option::is_none")]
298 #[ts(optional)]
299 pub set_code: Option<String>,
300 #[serde(default, skip_serializing_if = "Option::is_none")]
301 #[ts(optional)]
302 pub cube_id: Option<String>,
303 #[serde(default, skip_serializing_if = "Option::is_none")]
304 #[ts(optional)]
305 pub cube_name: Option<String>,
306 #[serde(default)]
307 pub singleton: bool,
308 pub num_boosters: u8,
309 #[serde(default, skip_serializing_if = "Option::is_none")]
310 #[ts(optional, type = "number")]
311 pub base_seed: Option<u64>,
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
315#[ts(export, export_to = "lobby/index.ts")]
316pub struct DraftConfig {
317 #[serde(default, skip_serializing_if = "Option::is_none")]
318 #[ts(optional)]
319 pub set_code: Option<String>,
320 #[serde(default, skip_serializing_if = "Option::is_none")]
321 #[ts(optional)]
322 pub cube_id: Option<String>,
323 #[serde(default, skip_serializing_if = "Option::is_none")]
324 #[ts(optional)]
325 pub cube_name: Option<String>,
326 pub rounds: u8,
327 pub picks_per_pass: u8,
328 #[serde(default, skip_serializing_if = "Option::is_none")]
329 #[ts(optional, type = "number")]
330 pub seed: Option<u64>,
331 pub fill_with_bots: bool,
332}
333
334#[derive(Debug, Clone, Serialize, Deserialize)]
335pub struct RoomPlayerInfo {
336 pub username: String,
337 pub ready: bool,
338 pub connected: bool,
339 #[serde(default)]
340 pub is_bot: bool,
341 #[serde(skip_serializing_if = "Option::is_none")]
342 pub selected_deck_name: Option<String>,
343}
344
345#[derive(Debug, Clone, Serialize, Deserialize)]
346pub struct PlayerInfo {
347 pub username: String,
348 pub player_id: String,
349 pub connected: bool,
350 #[serde(skip_serializing_if = "Option::is_none")]
351 pub room_id: Option<String>,
352}
353
354#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
355pub enum RoomStatus {
356 Lobby,
357 InGame,
358}
359
360#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
361#[ts(export, export_to = "lobby/index.ts")]
362pub enum GameFormat {
363 Any,
364 Standard,
365 Pioneer,
366 Modern,
367 Legacy,
368 Vintage,
369 Pauper,
370 Commander,
371 Brawl,
372 Oathbreaker,
373 Draft,
374 Sealed,
375}
376
377#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default, TS)]
378#[ts(export, export_to = "lobby/index.ts")]
379pub enum EngineKind {
380 #[default]
381 Manabrew,
382 Forge,
383 Ironsmith,
384}