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