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    /// Back face of a transform / modal_dfc card, captured at deck import.
59    /// Absent on single-faced cards and on decks saved before it existed.
60    #[serde(default, skip_serializing_if = "Option::is_none")]
61    #[ts(optional)]
62    pub back_face: Option<CardBackFaceSummary>,
63}
64
65#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
66#[serde(rename_all = "camelCase")]
67#[ts(export, export_to = "deck/index.ts")]
68pub struct CardBackFaceSummary {
69    pub name: String,
70    #[serde(default)]
71    pub mana_cost: String,
72    #[serde(default)]
73    pub type_line: String,
74    #[serde(default)]
75    pub oracle_text: String,
76    #[serde(default)]
77    pub uris: CardImageUris,
78}
79
80#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
81#[ts(export, export_to = "deck/index.ts")]
82pub struct CardImageUris {
83    #[serde(default)]
84    pub small: String,
85    #[serde(default)]
86    pub normal: String,
87    #[serde(default)]
88    pub large: String,
89    #[serde(default)]
90    pub png: String,
91    #[serde(default)]
92    pub art_crop: String,
93    #[serde(default)]
94    pub border_crop: String,
95}
96
97#[derive(Debug, Clone, Copy, Serialize, Deserialize, TS)]
98#[serde(rename_all = "snake_case")]
99#[ts(export, export_to = "deck/index.ts")]
100pub enum CardPartComponent {
101    Token,
102    ComboPiece,
103    MeldPart,
104    MeldResult,
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize, TS)]
108#[serde(rename_all = "camelCase")]
109#[ts(export, export_to = "deck/index.ts")]
110pub struct CardPart {
111    pub name: String,
112    pub component: CardPartComponent,
113}
114
115#[derive(Debug, Clone, Default, Serialize, TS)]
116#[serde(rename_all = "camelCase")]
117#[ts(export, export_to = "deck/index.ts")]
118pub struct DeckCard {
119    pub identity: DeckCardIdentity,
120    #[serde(flatten)]
121    #[ts(flatten)]
122    pub rules: CardRulesSummary,
123    #[serde(default)]
124    pub uris: CardImageUris,
125    #[serde(default, skip_serializing_if = "Option::is_none")]
126    #[ts(optional)]
127    pub all_parts: Option<Vec<CardPart>>,
128}
129
130pub const OUTDATED_CLIENT_MESSAGE: &str =
131    "this app version is out of date — download the latest release at manabrew.app to play online";
132
133// Clients ≤ v0.5.2 serialize the identity fields flattened onto the card.
134// Their gameplay wire (game view, prompts) has drifted too, so the legacy
135// shape is detected and rejected with an actionable message rather than
136// accepted into a game the client could not parse.
137#[derive(Deserialize)]
138#[serde(rename_all = "camelCase")]
139struct DeckCardWire {
140    #[serde(default)]
141    identity: Option<DeckCardIdentity>,
142    #[serde(default)]
143    name: Option<String>,
144    #[serde(flatten)]
145    rules: CardRulesSummary,
146    #[serde(default)]
147    uris: CardImageUris,
148    #[serde(default)]
149    all_parts: Option<Vec<CardPart>>,
150}
151
152impl<'de> Deserialize<'de> for DeckCard {
153    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
154        let wire = DeckCardWire::deserialize(deserializer)?;
155        let identity = match wire.identity {
156            Some(identity) => identity,
157            None if wire.name.is_some() => {
158                return Err(serde::de::Error::custom(OUTDATED_CLIENT_MESSAGE));
159            }
160            None => return Err(serde::de::Error::missing_field("identity")),
161        };
162        Ok(DeckCard {
163            identity,
164            rules: wire.rules,
165            uris: wire.uris,
166            all_parts: wire.all_parts,
167        })
168    }
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize, TS)]
172#[serde(rename_all = "camelCase")]
173#[ts(export, export_to = "deck/index.ts")]
174pub struct DeckLabel {
175    pub name: String,
176    #[serde(default, skip_serializing_if = "Option::is_none")]
177    #[ts(optional)]
178    pub color: Option<String>,
179}
180
181#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
182#[serde(rename_all = "camelCase")]
183#[ts(export, export_to = "deck/index.ts")]
184pub enum DeckFormat {
185    Standard,
186    Pioneer,
187    Modern,
188    Legacy,
189    Vintage,
190    Pauper,
191    Commander,
192    Brawl,
193    Oathbreaker,
194    Draft,
195    Sealed,
196}
197
198#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
199#[serde(rename_all = "camelCase")]
200#[ts(export, export_to = "deck/index.ts")]
201pub struct Deck {
202    #[serde(default, skip_serializing_if = "Option::is_none")]
203    #[ts(optional)]
204    pub version: Option<String>,
205    #[serde(default, skip_serializing_if = "Option::is_none")]
206    #[ts(optional)]
207    pub id: Option<String>,
208    pub name: String,
209    #[serde(default, skip_serializing_if = "Option::is_none")]
210    #[ts(optional)]
211    pub description: Option<String>,
212    #[serde(default, skip_serializing_if = "Option::is_none")]
213    #[ts(optional)]
214    pub color: Option<String>,
215    #[serde(default, skip_serializing_if = "Option::is_none")]
216    #[ts(optional)]
217    pub format: Option<DeckFormat>,
218    #[serde(default)]
219    pub cards: Vec<DeckCard>,
220    #[serde(default)]
221    pub sideboard: Vec<DeckCard>,
222    #[serde(default, skip_serializing_if = "Option::is_none")]
223    #[ts(optional)]
224    pub attractions: Option<Vec<DeckCard>>,
225    #[serde(default, skip_serializing_if = "Option::is_none")]
226    #[ts(optional)]
227    pub contraptions: Option<Vec<DeckCard>>,
228    #[serde(default, skip_serializing_if = "Option::is_none")]
229    #[ts(optional)]
230    pub schemes: Option<Vec<DeckCard>>,
231    #[serde(default, skip_serializing_if = "Option::is_none")]
232    #[ts(optional)]
233    pub planes: Option<Vec<DeckCard>>,
234    #[serde(default, skip_serializing_if = "Option::is_none")]
235    #[ts(optional)]
236    pub commanders: Option<Vec<DeckCard>>,
237    #[serde(default, skip_serializing_if = "Option::is_none")]
238    #[ts(optional)]
239    pub companion: Option<DeckCard>,
240    #[serde(default, skip_serializing_if = "Option::is_none")]
241    #[ts(optional)]
242    pub maybeboard: Option<Vec<DeckCard>>,
243    #[serde(default, skip_serializing_if = "Option::is_none")]
244    #[ts(optional)]
245    pub draft: Option<bool>,
246    #[serde(default, skip_serializing_if = "Option::is_none")]
247    #[ts(optional)]
248    pub labels: Option<Vec<DeckLabel>>,
249    #[serde(default, skip_serializing_if = "Option::is_none")]
250    #[ts(optional)]
251    pub cover_card_name: Option<String>,
252    #[serde(default, skip_serializing_if = "Option::is_none")]
253    #[ts(optional)]
254    pub cover_card_face: Option<u8>,
255    #[serde(default, skip_serializing_if = "Option::is_none")]
256    #[ts(optional)]
257    pub playmat: Option<String>,
258    #[serde(default, skip_serializing_if = "Option::is_none")]
259    #[ts(optional)]
260    pub playmat_settings: Option<PlaymatSettings>,
261    #[serde(default, skip_serializing_if = "Option::is_none")]
262    #[ts(optional, type = "Record<string, { x: number; y: number }>")]
263    pub stack_positions: Option<serde_json::Value>,
264    #[serde(default, skip_serializing_if = "Option::is_none")]
265    #[ts(optional)]
266    pub tokens: Option<Vec<DeckCard>>,
267}