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 CreateRoom {
108 room_name: String,
109 max_players: u8,
110 format: GameFormat,
111 #[serde(default)]
112 protocol_version: u32,
113 #[serde(default)]
114 hosted: bool,
115 #[serde(default)]
116 engine: EngineKind,
117 #[serde(default)]
118 draft_config: Option<DraftConfig>,
119 #[serde(default)]
120 sealed_config: Option<SealedConfig>,
121 #[serde(default)]
122 official_key: Option<String>,
123 #[serde(default)]
124 password: Option<String>,
125 #[serde(default)]
126 reconnect_timeout_s: Option<u32>,
127 },
128
129 JoinRoom {
130 room_id: String,
131 #[serde(default)]
132 observe: bool,
133 #[serde(default)]
134 as_bot: bool,
135 #[serde(default)]
136 password: Option<String>,
137 },
138
139 ResumeRoom(ResumeRoomRequest),
140
141 LeaveRoom,
142
143 SetReady {
144 ready: bool,
145 },
146
147 SetDeckSelection {
148 deck_name: String,
149 deck: Deck,
150 #[serde(default)]
151 published_deck_id: Option<String>,
152 commander_name: Option<String>,
153 #[serde(default)]
154 avatar_url: Option<String>,
155 },
156
157 SetFormat {
158 format: GameFormat,
159 },
160
161 SetMaxPlayers {
162 max_players: u8,
163 },
164
165 StartGame {
166 #[serde(default)]
167 format: Option<GameFormat>,
168 },
169
170 EndGame {
171 game_id: String,
172 },
173
174 ReportEngineStats {
175 #[serde(default, skip_serializing_if = "Option::is_none")]
176 game_id: Option<String>,
177 stats: EnginePlayStats,
178 },
179
180 RequestResync,
181
182 BroadcastState {
183 state: serde_json::Value,
184 #[serde(default, skip_serializing_if = "Option::is_none")]
185 target_player: Option<String>,
187 },
188
189 TurnChange {
190 new_active_player: String,
191 turn_number: u32,
192 },
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize)]
196#[serde(tag = "type")]
197pub enum ServerMessage {
198 Pong,
199
200 AuthResult {
201 success: bool,
202 player_id: Option<String>,
203 reconnected: Option<bool>,
204 error: Option<String>,
205 },
206
207 SessionTakenOver,
208
209 RoomList {
210 rooms: Vec<RoomInfo>,
211 },
212
213 PlayerList {
214 players: Vec<PlayerInfo>,
215 },
216
217 RoomCreated {
218 room_id: String,
219 room_name: String,
220 room: RoomInfo,
221 #[serde(default)]
222 resume_token: Option<String>,
223 },
224
225 RoomResumed {
226 room: RoomInfo,
227 },
228
229 PlayerJoined {
230 room_id: String,
231 username: String,
232 },
233
234 PlayerLeft {
235 room_id: String,
236 username: String,
237 },
238
239 PlayerConnected {
240 username: String,
241 },
242
243 PlayerDisconnected {
244 username: String,
245 },
246
247 ReadyStateChanged {
248 username: String,
249 ready: bool,
250 },
251
252 RoomUpdate {
253 room: RoomInfo,
254 },
255
256 GameStarted {
257 room_id: String,
258 game_id: String,
259 player_order: Vec<String>,
260 player_decks: Vec<PlayerDeckInfo>,
261 starting_life: i32,
262 },
263
264 StateUpdate {
265 from_player: String,
266 state: serde_json::Value,
267 },
268
269 TurnChanged {
270 from_player: String,
271 new_active_player: String,
272 turn_number: u32,
273 },
274
275 GameAborted {
276 room_id: String,
277 },
278
279 Error {
280 code: String,
281 message: String,
282 },
283
284 ServerShuttingDown {
285 reconnect_in_s: u32,
286 },
287}
288
289#[derive(Debug, Clone, Serialize, Deserialize, TS)]
290#[ts(export, export_to = "lobby/index.ts")]
291pub struct ResumeRoomRequest {
292 pub room_id: String,
293 pub resume_token: String,
294 pub room_name: String,
295 pub max_players: u8,
296 pub format: GameFormat,
297 #[serde(default)]
298 pub hosted: bool,
299 #[serde(default)]
300 pub engine: EngineKind,
301 #[serde(default, skip_serializing_if = "Option::is_none")]
302 #[ts(optional)]
303 pub official_key: Option<String>,
304 #[serde(default, skip_serializing_if = "Option::is_none")]
305 #[ts(optional)]
306 pub password: Option<String>,
307 #[serde(default, skip_serializing_if = "Option::is_none")]
308 #[ts(optional)]
309 pub reconnect_timeout_s: Option<u32>,
310 #[serde(default, skip_serializing_if = "Option::is_none")]
311 #[ts(optional)]
312 pub draft_config: Option<DraftConfig>,
313 #[serde(default, skip_serializing_if = "Option::is_none")]
314 #[ts(optional)]
315 pub sealed_config: Option<SealedConfig>,
316 pub player_order: Vec<String>,
317 pub player_decks: Vec<PlayerDeckInfo>,
318 pub starting_life: i32,
319 #[serde(default)]
320 pub bot_players: Vec<String>,
321 pub game_id: String,
322}
323
324#[derive(Debug, Clone, Serialize, Deserialize)]
325pub struct RoomInfo {
326 pub room_id: String,
327 pub room_name: String,
328 pub host: String,
329 #[serde(default)]
330 pub protocol_version: u32,
331 #[serde(default)]
332 pub hosted: bool,
333 #[serde(default)]
334 pub official: bool,
335 #[serde(default)]
336 pub password_protected: bool,
337 pub players: Vec<RoomPlayerInfo>,
338 pub max_players: u8,
339 pub format: GameFormat,
340 pub status: RoomStatus,
341 #[serde(default)]
342 pub engine: EngineKind,
343 #[serde(default = "default_reconnect_timeout_s")]
344 pub reconnect_timeout_s: u32,
345 #[serde(default, skip_serializing_if = "Option::is_none")]
346 pub draft_config: Option<DraftConfig>,
347 #[serde(default, skip_serializing_if = "Option::is_none")]
348 pub sealed_config: Option<SealedConfig>,
349}
350
351pub const DEFAULT_RECONNECT_TIMEOUT_S: u32 = 60;
352
353fn default_reconnect_timeout_s() -> u32 {
354 DEFAULT_RECONNECT_TIMEOUT_S
355}
356
357#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
358#[ts(export, export_to = "lobby/index.ts")]
359pub struct SealedConfig {
360 #[serde(default, skip_serializing_if = "Option::is_none")]
361 #[ts(optional)]
362 pub set_code: Option<String>,
363 #[serde(default, skip_serializing_if = "Option::is_none")]
364 #[ts(optional)]
365 pub cube_id: Option<String>,
366 #[serde(default, skip_serializing_if = "Option::is_none")]
367 #[ts(optional)]
368 pub cube_name: Option<String>,
369 #[serde(default)]
370 pub singleton: bool,
371 pub num_boosters: u8,
372 #[serde(default, skip_serializing_if = "Option::is_none")]
373 #[ts(optional, type = "number")]
374 pub base_seed: Option<u64>,
375}
376
377#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
378#[ts(export, export_to = "lobby/index.ts")]
379pub struct DraftConfig {
380 #[serde(default, skip_serializing_if = "Option::is_none")]
381 #[ts(optional)]
382 pub set_code: Option<String>,
383 #[serde(default, skip_serializing_if = "Option::is_none")]
384 #[ts(optional)]
385 pub cube_id: Option<String>,
386 #[serde(default, skip_serializing_if = "Option::is_none")]
387 #[ts(optional)]
388 pub cube_name: Option<String>,
389 pub rounds: u8,
390 pub picks_per_pass: u8,
391 #[serde(default, skip_serializing_if = "Option::is_none")]
392 #[ts(optional, type = "number")]
393 pub seed: Option<u64>,
394 pub fill_with_bots: bool,
395}
396
397#[derive(Debug, Clone, Serialize, Deserialize)]
398pub struct RoomPlayerInfo {
399 pub username: String,
400 pub ready: bool,
401 pub connected: bool,
402 #[serde(default)]
403 pub is_bot: bool,
404 #[serde(skip_serializing_if = "Option::is_none")]
405 pub selected_deck_name: Option<String>,
406}
407
408#[derive(Debug, Clone, Serialize, Deserialize)]
409pub struct PlayerInfo {
410 pub username: String,
411 pub player_id: String,
412 pub connected: bool,
413 #[serde(default)]
414 pub verified: bool,
415 #[serde(default, skip_serializing_if = "Option::is_none")]
416 pub qualification: Option<String>,
417 #[serde(skip_serializing_if = "Option::is_none")]
418 pub room_id: Option<String>,
419}
420
421#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
422pub enum RoomStatus {
423 Lobby,
424 InGame,
425}