1use serde::{Deserialize, Serialize};
2use sha2::{Digest, Sha256};
3use std::fmt::Write;
4use ts_rs::TS;
5
6use crate::{game::PlaymatSettings, TokenScript};
7
8#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
9#[serde(rename_all = "camelCase")]
10#[ts(export, export_to = "deck/index.ts")]
11pub struct DeckCardIdentity {
12 #[serde(default, skip_serializing_if = "String::is_empty")]
13 pub id: String,
14 pub name: String,
15 pub set_code: String,
16 pub card_number: String,
17 #[serde(default, skip_serializing_if = "Option::is_none")]
18 #[ts(optional)]
19 pub oracle_id: Option<String>,
20 #[serde(default, skip_serializing_if = "Option::is_none")]
21 #[ts(optional)]
22 pub token_script: Option<TokenScript>,
23 #[serde(default, skip_serializing_if = "Option::is_none")]
24 #[ts(optional)]
25 pub foil: Option<bool>,
26}
27
28#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
32#[serde(rename_all = "camelCase")]
33#[ts(export, export_to = "deck/index.ts")]
34pub struct CardRulesSummary {
35 #[serde(default)]
36 pub color: String,
37 #[serde(default)]
38 pub color_identity: Vec<String>,
39 #[serde(default)]
40 pub mana_cost: String,
41 #[serde(default)]
42 pub cmc: f32,
43 #[serde(default)]
44 pub types: Vec<String>,
45 #[serde(default)]
46 pub subtypes: Vec<String>,
47 #[serde(default)]
48 pub supertypes: Vec<String>,
49 #[serde(default, skip_serializing_if = "Option::is_none")]
50 #[ts(optional)]
51 pub keywords: Option<Vec<String>>,
52 #[serde(default, skip_serializing_if = "Option::is_none")]
53 #[ts(optional)]
54 pub power: Option<String>,
55 #[serde(default, skip_serializing_if = "Option::is_none")]
56 #[ts(optional)]
57 pub toughness: Option<String>,
58 #[serde(default)]
59 pub text: String,
60 #[serde(default, skip_serializing_if = "Option::is_none")]
61 #[ts(optional)]
62 pub layout: Option<String>,
63 #[serde(default, skip_serializing_if = "Option::is_none")]
64 #[ts(optional)]
65 pub is_double_faced: Option<bool>,
66 #[serde(default, skip_serializing_if = "Option::is_none")]
69 #[ts(optional)]
70 pub back_face: Option<CardBackFaceSummary>,
71}
72
73#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
74#[serde(rename_all = "camelCase")]
75#[ts(export, export_to = "deck/index.ts")]
76pub struct CardBackFaceSummary {
77 pub name: String,
78 #[serde(default)]
79 pub mana_cost: String,
80 #[serde(default)]
81 pub type_line: String,
82 #[serde(default)]
83 pub oracle_text: String,
84 #[serde(default)]
85 pub uris: CardImageUris,
86}
87
88#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
89#[ts(export, export_to = "deck/index.ts")]
90pub struct CardImageUris {
91 #[serde(default)]
92 pub small: String,
93 #[serde(default)]
94 pub normal: String,
95 #[serde(default)]
96 pub large: String,
97 #[serde(default)]
98 pub png: String,
99 #[serde(default)]
100 pub art_crop: String,
101 #[serde(default)]
102 pub border_crop: String,
103}
104
105#[derive(Debug, Clone, Copy, Serialize, Deserialize, TS)]
106#[serde(rename_all = "snake_case")]
107#[ts(export, export_to = "deck/index.ts")]
108pub enum CardPartComponent {
109 Token,
110 ComboPiece,
111 MeldPart,
112 MeldResult,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize, TS)]
116#[serde(rename_all = "camelCase")]
117#[ts(export, export_to = "deck/index.ts")]
118pub struct CardPart {
119 pub name: String,
120 pub component: CardPartComponent,
121}
122
123#[derive(Debug, Clone, Default, Serialize, TS)]
124#[serde(rename_all = "camelCase")]
125#[ts(export, export_to = "deck/index.ts")]
126pub struct DeckCard {
127 pub identity: DeckCardIdentity,
128 #[serde(flatten)]
129 #[ts(flatten)]
130 pub rules: CardRulesSummary,
131 #[serde(default)]
132 pub uris: CardImageUris,
133 #[serde(default, skip_serializing_if = "Option::is_none")]
134 #[ts(optional)]
135 pub all_parts: Option<Vec<CardPart>>,
136}
137
138pub const OUTDATED_CLIENT_MESSAGE: &str =
139 "this app version is out of date — download the latest release at manabrew.app to play online";
140
141#[derive(Deserialize)]
146#[serde(rename_all = "camelCase")]
147struct DeckCardWire {
148 #[serde(default)]
149 identity: Option<DeckCardIdentity>,
150 #[serde(default)]
151 name: Option<String>,
152 #[serde(flatten)]
153 rules: CardRulesSummary,
154 #[serde(default)]
155 uris: CardImageUris,
156 #[serde(default)]
157 all_parts: Option<Vec<CardPart>>,
158}
159
160impl<'de> Deserialize<'de> for DeckCard {
161 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
162 let wire = DeckCardWire::deserialize(deserializer)?;
163 let identity = match wire.identity {
164 Some(identity) => identity,
165 None if wire.name.is_some() => {
166 return Err(serde::de::Error::custom(OUTDATED_CLIENT_MESSAGE));
167 }
168 None => return Err(serde::de::Error::missing_field("identity")),
169 };
170 Ok(DeckCard {
171 identity,
172 rules: wire.rules,
173 uris: wire.uris,
174 all_parts: wire.all_parts,
175 })
176 }
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize, TS)]
180#[serde(rename_all = "camelCase")]
181#[ts(export, export_to = "deck/index.ts")]
182pub struct DeckLabel {
183 pub name: String,
184 #[serde(default, skip_serializing_if = "Option::is_none")]
185 #[ts(optional)]
186 pub color: Option<String>,
187}
188
189#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
190#[serde(rename_all = "camelCase")]
191#[ts(export, export_to = "deck/index.ts")]
192pub enum DeckFormat {
193 Standard,
194 Pioneer,
195 Modern,
196 Legacy,
197 Vintage,
198 Pauper,
199 Commander,
200 Brawl,
201 Oathbreaker,
202 Draft,
203 Sealed,
204}
205
206#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
207#[serde(rename_all = "camelCase")]
208#[ts(export, export_to = "deck/index.ts")]
209pub struct Deck {
210 #[serde(default, skip_serializing_if = "Option::is_none")]
211 #[ts(optional)]
212 pub version: Option<String>,
213 #[serde(default, skip_serializing_if = "Option::is_none")]
214 #[ts(optional)]
215 pub id: Option<String>,
216 pub name: String,
217 #[serde(default, skip_serializing_if = "Option::is_none")]
218 #[ts(optional)]
219 pub description: Option<String>,
220 #[serde(default, skip_serializing_if = "Option::is_none")]
221 #[ts(optional)]
222 pub color: Option<String>,
223 #[serde(default, skip_serializing_if = "Option::is_none")]
224 #[ts(optional)]
225 pub format: Option<DeckFormat>,
226 #[serde(default)]
227 pub cards: Vec<DeckCard>,
228 #[serde(default)]
229 pub sideboard: Vec<DeckCard>,
230 #[serde(default, skip_serializing_if = "Option::is_none")]
231 #[ts(optional)]
232 pub attractions: Option<Vec<DeckCard>>,
233 #[serde(default, skip_serializing_if = "Option::is_none")]
234 #[ts(optional)]
235 pub contraptions: Option<Vec<DeckCard>>,
236 #[serde(default, skip_serializing_if = "Option::is_none")]
237 #[ts(optional)]
238 pub schemes: Option<Vec<DeckCard>>,
239 #[serde(default, skip_serializing_if = "Option::is_none")]
240 #[ts(optional)]
241 pub planes: Option<Vec<DeckCard>>,
242 #[serde(default, skip_serializing_if = "Option::is_none")]
243 #[ts(optional)]
244 pub commanders: Option<Vec<DeckCard>>,
245 #[serde(default, skip_serializing_if = "Option::is_none")]
246 #[ts(optional)]
247 pub companion: Option<DeckCard>,
248 #[serde(default, skip_serializing_if = "Option::is_none")]
249 #[ts(optional)]
250 pub maybeboard: Option<Vec<DeckCard>>,
251 #[serde(default, skip_serializing_if = "Option::is_none")]
252 #[ts(optional)]
253 pub draft: Option<bool>,
254 #[serde(default, skip_serializing_if = "Option::is_none")]
255 #[ts(optional)]
256 pub labels: Option<Vec<DeckLabel>>,
257 #[serde(default, skip_serializing_if = "Option::is_none")]
258 #[ts(optional)]
259 pub cover_card_name: Option<String>,
260 #[serde(default, skip_serializing_if = "Option::is_none")]
261 #[ts(optional)]
262 pub cover_card_face: Option<u8>,
263 #[serde(default, skip_serializing_if = "Option::is_none")]
264 #[ts(optional)]
265 pub playmat: Option<String>,
266 #[serde(default, skip_serializing_if = "Option::is_none")]
267 #[ts(optional)]
268 pub playmat_settings: Option<PlaymatSettings>,
269 #[serde(default, skip_serializing_if = "Option::is_none")]
270 #[ts(optional, type = "Record<string, { x: number; y: number }>")]
271 pub stack_positions: Option<serde_json::Value>,
272 #[serde(default, skip_serializing_if = "Option::is_none")]
273 #[ts(optional)]
274 pub tokens: Option<Vec<DeckCard>>,
275}
276
277pub fn deck_fingerprint(deck: &Deck) -> String {
278 let mut hasher = Sha256::new();
279 update_fingerprint(&mut hasher, &deck.name);
280 update_fingerprint(
281 &mut hasher,
282 &serde_json::to_string(&deck.format).unwrap_or_default(),
283 );
284 let mut cards = Vec::new();
285 cards.extend(deck.cards.iter().map(|card| ("main", card)));
286 cards.extend(deck.sideboard.iter().map(|card| ("sideboard", card)));
287 cards.extend(
288 deck.attractions
289 .iter()
290 .flatten()
291 .map(|card| ("attraction", card)),
292 );
293 cards.extend(
294 deck.contraptions
295 .iter()
296 .flatten()
297 .map(|card| ("contraption", card)),
298 );
299 cards.extend(deck.schemes.iter().flatten().map(|card| ("scheme", card)));
300 cards.extend(deck.planes.iter().flatten().map(|card| ("plane", card)));
301 cards.extend(
302 deck.commanders
303 .iter()
304 .flatten()
305 .map(|card| ("commander", card)),
306 );
307 cards.extend(deck.companion.iter().map(|card| ("companion", card)));
308 cards.sort_by(|(left_section, left), (right_section, right)| {
309 (
310 left_section,
311 &left.identity.name,
312 &left.identity.set_code,
313 &left.identity.card_number,
314 )
315 .cmp(&(
316 right_section,
317 &right.identity.name,
318 &right.identity.set_code,
319 &right.identity.card_number,
320 ))
321 });
322 for (section, card) in cards {
323 update_fingerprint(&mut hasher, section);
324 update_fingerprint(&mut hasher, &card.identity.name);
325 update_fingerprint(&mut hasher, &card.identity.set_code);
326 update_fingerprint(&mut hasher, &card.identity.card_number);
327 }
328 let digest = hasher.finalize();
329 let mut fingerprint = String::with_capacity(digest.len() * 2);
330 for byte in digest {
331 let _ = write!(fingerprint, "{byte:02x}");
332 }
333 fingerprint
334}
335
336fn update_fingerprint(hasher: &mut Sha256, value: &str) {
337 hasher.update((value.len() as u64).to_le_bytes());
338 hasher.update(value.as_bytes());
339}