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