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 },
219
220 SessionTakenOver,
221
222 RoomList {
223 rooms: Vec<RoomInfo>,
224 },
225
226 PlayerList {
227 players: Vec<PlayerInfo>,
228 },
229
230 RoomCreated {
231 room_id: String,
232 room_name: String,
233 room: RoomInfo,
234 #[serde(default)]
235 resume_token: Option<String>,
236 },
237
238 RoomResumed {
239 room: RoomInfo,
240 },
241
242 PlayerJoined {
243 room_id: String,
244 username: String,
245 },
246
247 PlayerLeft {
248 room_id: String,
249 username: String,
250 },
251
252 PlayerConnected {
253 username: String,
254 },
255
256 PlayerDisconnected {
257 username: String,
258 },
259
260 ReadyStateChanged {
261 username: String,
262 ready: bool,
263 },
264
265 RoomUpdate {
266 room: RoomInfo,
267 },
268
269 GameStarted {
270 room_id: String,
271 game_id: String,
272 player_order: Vec<String>,
273 player_decks: Vec<PlayerDeckInfo>,
274 starting_life: i32,
275 },
276
277 StateUpdate {
278 from_player: String,
279 state: serde_json::Value,
280 },
281
282 TurnChanged {
283 from_player: String,
284 new_active_player: String,
285 turn_number: u32,
286 },
287
288 GameAborted {
289 room_id: String,
290 },
291
292 Error {
293 code: String,
294 message: String,
295 },
296
297 ServerShuttingDown {
298 reconnect_in_s: u32,
299 },
300}
301
302#[derive(Debug, Clone, Serialize, Deserialize, TS)]
303#[ts(export, export_to = "lobby/index.ts")]
304pub struct ResumeRoomRequest {
305 pub room_id: String,
306 pub resume_token: String,
307 pub room_name: String,
308 pub max_players: u8,
309 pub format: GameFormat,
310 #[serde(default)]
311 pub hosted: bool,
312 #[serde(default)]
313 pub engine: EngineKind,
314 #[serde(default, skip_serializing_if = "Option::is_none")]
315 #[ts(optional)]
316 pub official_key: Option<String>,
317 #[serde(default, skip_serializing_if = "Option::is_none")]
318 #[ts(optional)]
319 pub password: Option<String>,
320 #[serde(default, skip_serializing_if = "Option::is_none")]
321 #[ts(optional)]
322 pub reconnect_timeout_s: Option<u32>,
323 #[serde(default, skip_serializing_if = "Option::is_none")]
324 #[ts(optional)]
325 pub draft_config: Option<DraftConfig>,
326 #[serde(default, skip_serializing_if = "Option::is_none")]
327 #[ts(optional)]
328 pub sealed_config: Option<SealedConfig>,
329 pub player_order: Vec<String>,
330 pub player_decks: Vec<PlayerDeckInfo>,
331 pub starting_life: i32,
332 #[serde(default)]
333 pub bot_players: Vec<String>,
334 pub game_id: String,
335}
336
337#[derive(Debug, Clone, Serialize, Deserialize)]
338pub struct RoomInfo {
339 pub room_id: String,
340 pub room_name: String,
341 pub host: String,
342 #[serde(default)]
343 pub protocol_version: u32,
344 #[serde(default)]
345 pub hosted: bool,
346 #[serde(default)]
347 pub official: bool,
348 #[serde(default)]
349 pub password_protected: bool,
350 pub players: Vec<RoomPlayerInfo>,
351 pub max_players: u8,
352 pub format: GameFormat,
353 pub status: RoomStatus,
354 #[serde(default)]
355 pub engine: EngineKind,
356 #[serde(default = "default_reconnect_timeout_s")]
357 pub reconnect_timeout_s: u32,
358 #[serde(default, skip_serializing_if = "Option::is_none")]
359 pub draft_config: Option<DraftConfig>,
360 #[serde(default, skip_serializing_if = "Option::is_none")]
361 pub sealed_config: Option<SealedConfig>,
362}
363
364pub const DEFAULT_RECONNECT_TIMEOUT_S: u32 = 60;
365
366fn default_reconnect_timeout_s() -> u32 {
367 DEFAULT_RECONNECT_TIMEOUT_S
368}
369
370#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
371#[ts(export, export_to = "lobby/index.ts")]
372pub struct SealedConfig {
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 #[serde(default)]
383 pub singleton: bool,
384 pub num_boosters: u8,
385 #[serde(default, skip_serializing_if = "Option::is_none")]
386 #[ts(optional, type = "number")]
387 pub base_seed: Option<u64>,
388}
389
390#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
391#[ts(export, export_to = "lobby/index.ts")]
392pub struct DraftConfig {
393 #[serde(default, skip_serializing_if = "Option::is_none")]
394 #[ts(optional)]
395 pub set_code: Option<String>,
396 #[serde(default, skip_serializing_if = "Option::is_none")]
397 #[ts(optional)]
398 pub cube_id: Option<String>,
399 #[serde(default, skip_serializing_if = "Option::is_none")]
400 #[ts(optional)]
401 pub cube_name: Option<String>,
402 pub rounds: u8,
403 pub picks_per_pass: u8,
404 #[serde(default, skip_serializing_if = "Option::is_none")]
405 #[ts(optional, type = "number")]
406 pub seed: Option<u64>,
407 pub fill_with_bots: bool,
408}
409
410#[derive(Debug, Clone, Serialize, Deserialize)]
411pub struct RoomPlayerInfo {
412 pub username: String,
413 pub ready: bool,
414 pub connected: bool,
415 #[serde(default)]
416 pub is_bot: bool,
417 #[serde(skip_serializing_if = "Option::is_none")]
418 pub selected_deck_name: Option<String>,
419}
420
421#[derive(Debug, Clone, Serialize, Deserialize)]
422pub struct PlayerInfo {
423 pub username: String,
424 pub player_id: String,
425 pub connected: bool,
426 #[serde(default)]
427 pub verified: bool,
428 #[serde(default, skip_serializing_if = "Option::is_none")]
429 pub qualification: Option<String>,
430 #[serde(default, skip_serializing_if = "Option::is_none")]
431 pub avatar_url: Option<String>,
432 #[serde(skip_serializing_if = "Option::is_none")]
433 pub room_id: Option<String>,
434 #[serde(default, skip_serializing_if = "Option::is_none")]
437 pub local_game: Option<LocalGameKind>,
438}
439
440pub const FEATURE_LOCAL_GAME: &str = "local_game";
442
443pub const FEATURES: &[&str] = &[FEATURE_LOCAL_GAME];
444
445#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
447pub enum LocalGameKind {
448 Singleplayer,
450}
451
452impl LocalGameKind {
453 pub const ALL: &'static [Self] = &[Self::Singleplayer];
456
457 pub fn as_str(self) -> &'static str {
458 match self {
459 Self::Singleplayer => "singleplayer",
460 }
461 }
462}
463
464#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
465pub enum RoomStatus {
466 Lobby,
467 InGame,
468}