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