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