Skip to main content

manabrew_protocol/
deck_dto.rs

1use serde::{Deserialize, Serialize};
2use ts_rs::TS;
3
4use crate::game::PlaymatSettings;
5
6#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
7#[serde(rename_all = "camelCase")]
8#[ts(export, export_to = "deck/index.ts")]
9pub struct DeckCardIdentity {
10    #[serde(default, skip_serializing_if = "String::is_empty")]
11    pub id: String,
12    pub name: String,
13    pub set_code: String,
14    pub card_number: String,
15    #[serde(default, skip_serializing_if = "Option::is_none")]
16    #[ts(optional)]
17    pub foil: Option<bool>,
18}
19
20/// Mirror of `manabrew.ts:CardRulesSummary`. The engine derives most
21/// of this from its own card DB; included here so the same wire shape
22/// round-trips losslessly.
23#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
24#[serde(rename_all = "camelCase")]
25#[ts(export, export_to = "deck/index.ts")]
26pub struct CardRulesSummary {
27    #[serde(default)]
28    pub color: String,
29    #[serde(default)]
30    pub color_identity: Vec<String>,
31    #[serde(default)]
32    pub mana_cost: String,
33    #[serde(default)]
34    pub cmc: f32,
35    #[serde(default)]
36    pub types: Vec<String>,
37    #[serde(default)]
38    pub subtypes: Vec<String>,
39    #[serde(default)]
40    pub supertypes: Vec<String>,
41    #[serde(default, skip_serializing_if = "Option::is_none")]
42    #[ts(optional)]
43    pub keywords: Option<Vec<String>>,
44    #[serde(default, skip_serializing_if = "Option::is_none")]
45    #[ts(optional)]
46    pub power: Option<String>,
47    #[serde(default, skip_serializing_if = "Option::is_none")]
48    #[ts(optional)]
49    pub toughness: Option<String>,
50    #[serde(default)]
51    pub text: String,
52    #[serde(default, skip_serializing_if = "Option::is_none")]
53    #[ts(optional)]
54    pub layout: Option<String>,
55    #[serde(default, skip_serializing_if = "Option::is_none")]
56    #[ts(optional)]
57    pub is_double_faced: Option<bool>,
58}
59
60#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
61#[ts(export, export_to = "deck/index.ts")]
62pub struct CardImageUris {
63    #[serde(default)]
64    pub small: String,
65    #[serde(default)]
66    pub normal: String,
67    #[serde(default)]
68    pub large: String,
69    #[serde(default)]
70    pub png: String,
71    #[serde(default)]
72    pub art_crop: String,
73    #[serde(default)]
74    pub border_crop: String,
75}
76
77#[derive(Debug, Clone, Copy, Serialize, Deserialize, TS)]
78#[serde(rename_all = "snake_case")]
79#[ts(export, export_to = "deck/index.ts")]
80pub enum CardPartComponent {
81    Token,
82    ComboPiece,
83    MeldPart,
84    MeldResult,
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize, TS)]
88#[serde(rename_all = "camelCase")]
89#[ts(export, export_to = "deck/index.ts")]
90pub struct CardPart {
91    pub name: String,
92    pub component: CardPartComponent,
93}
94
95#[derive(Debug, Clone, Default, Serialize, TS)]
96#[serde(rename_all = "camelCase")]
97#[ts(export, export_to = "deck/index.ts")]
98pub struct DeckCard {
99    pub identity: DeckCardIdentity,
100    #[serde(flatten)]
101    #[ts(flatten)]
102    pub rules: CardRulesSummary,
103    #[serde(default)]
104    pub uris: CardImageUris,
105    #[serde(default, skip_serializing_if = "Option::is_none")]
106    #[ts(optional)]
107    pub all_parts: Option<Vec<CardPart>>,
108}
109
110pub const OUTDATED_CLIENT_MESSAGE: &str =
111    "this app version is out of date — download the latest release at manabrew.app to play online";
112
113// Clients ≤ v0.5.2 serialize the identity fields flattened onto the card.
114// Their gameplay wire (game view, prompts) has drifted too, so the legacy
115// shape is detected and rejected with an actionable message rather than
116// accepted into a game the client could not parse.
117#[derive(Deserialize)]
118#[serde(rename_all = "camelCase")]
119struct DeckCardWire {
120    #[serde(default)]
121    identity: Option<DeckCardIdentity>,
122    #[serde(default)]
123    name: Option<String>,
124    #[serde(flatten)]
125    rules: CardRulesSummary,
126    #[serde(default)]
127    uris: CardImageUris,
128    #[serde(default)]
129    all_parts: Option<Vec<CardPart>>,
130}
131
132impl<'de> Deserialize<'de> for DeckCard {
133    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
134        let wire = DeckCardWire::deserialize(deserializer)?;
135        let identity = match wire.identity {
136            Some(identity) => identity,
137            None if wire.name.is_some() => {
138                return Err(serde::de::Error::custom(OUTDATED_CLIENT_MESSAGE));
139            }
140            None => return Err(serde::de::Error::missing_field("identity")),
141        };
142        Ok(DeckCard {
143            identity,
144            rules: wire.rules,
145            uris: wire.uris,
146            all_parts: wire.all_parts,
147        })
148    }
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize, TS)]
152#[serde(rename_all = "camelCase")]
153#[ts(export, export_to = "deck/index.ts")]
154pub struct DeckLabel {
155    pub name: String,
156    #[serde(default, skip_serializing_if = "Option::is_none")]
157    #[ts(optional)]
158    pub color: Option<String>,
159}
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
162#[serde(rename_all = "camelCase")]
163#[ts(export, export_to = "deck/index.ts")]
164pub enum DeckFormat {
165    Standard,
166    Pioneer,
167    Modern,
168    Legacy,
169    Vintage,
170    Pauper,
171    Commander,
172    Brawl,
173    Oathbreaker,
174    Draft,
175    Sealed,
176}
177
178#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
179#[serde(rename_all = "camelCase")]
180#[ts(export, export_to = "deck/index.ts")]
181pub struct Deck {
182    #[serde(default, skip_serializing_if = "Option::is_none")]
183    #[ts(optional)]
184    pub version: Option<String>,
185    #[serde(default, skip_serializing_if = "Option::is_none")]
186    #[ts(optional)]
187    pub id: Option<String>,
188    pub name: String,
189    #[serde(default, skip_serializing_if = "Option::is_none")]
190    #[ts(optional)]
191    pub description: Option<String>,
192    #[serde(default, skip_serializing_if = "Option::is_none")]
193    #[ts(optional)]
194    pub color: Option<String>,
195    #[serde(default, skip_serializing_if = "Option::is_none")]
196    #[ts(optional)]
197    pub format: Option<DeckFormat>,
198    #[serde(default)]
199    pub cards: Vec<DeckCard>,
200    #[serde(default)]
201    pub sideboard: Vec<DeckCard>,
202    #[serde(default, skip_serializing_if = "Option::is_none")]
203    #[ts(optional)]
204    pub attractions: Option<Vec<DeckCard>>,
205    #[serde(default, skip_serializing_if = "Option::is_none")]
206    #[ts(optional)]
207    pub contraptions: Option<Vec<DeckCard>>,
208    #[serde(default, skip_serializing_if = "Option::is_none")]
209    #[ts(optional)]
210    pub schemes: Option<Vec<DeckCard>>,
211    #[serde(default, skip_serializing_if = "Option::is_none")]
212    #[ts(optional)]
213    pub planes: Option<Vec<DeckCard>>,
214    #[serde(default, skip_serializing_if = "Option::is_none")]
215    #[ts(optional)]
216    pub commanders: Option<Vec<DeckCard>>,
217    #[serde(default, skip_serializing_if = "Option::is_none")]
218    #[ts(optional)]
219    pub companion: Option<DeckCard>,
220    #[serde(default, skip_serializing_if = "Option::is_none")]
221    #[ts(optional)]
222    pub maybeboard: Option<Vec<DeckCard>>,
223    #[serde(default, skip_serializing_if = "Option::is_none")]
224    #[ts(optional)]
225    pub draft: Option<bool>,
226    #[serde(default, skip_serializing_if = "Option::is_none")]
227    #[ts(optional)]
228    pub labels: Option<Vec<DeckLabel>>,
229    #[serde(default, skip_serializing_if = "Option::is_none")]
230    #[ts(optional)]
231    pub cover_card_name: Option<String>,
232    #[serde(default, skip_serializing_if = "Option::is_none")]
233    #[ts(optional)]
234    pub cover_card_face: Option<u8>,
235    #[serde(default, skip_serializing_if = "Option::is_none")]
236    #[ts(optional)]
237    pub playmat: Option<String>,
238    #[serde(default, skip_serializing_if = "Option::is_none")]
239    #[ts(optional)]
240    pub playmat_settings: Option<PlaymatSettings>,
241    #[serde(default, skip_serializing_if = "Option::is_none")]
242    #[ts(optional, type = "Record<string, { x: number; y: number }>")]
243    pub stack_positions: Option<serde_json::Value>,
244    #[serde(default, skip_serializing_if = "Option::is_none")]
245    #[ts(optional)]
246    pub tokens: Option<Vec<DeckCard>>,
247}