Skip to main content

manabrew_protocol/
protocol.rs

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