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