1use serde::{Deserialize, Serialize};
2use sha2::{Digest, Sha256};
3use std::collections::BTreeMap;
4use std::fmt::Write;
5use ts_rs::TS;
6
7use crate::{game::PlaymatSettings, TokenScript};
8
9#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
10#[serde(rename_all = "camelCase")]
11#[ts(export, export_to = "deck/index.ts")]
12pub struct DeckCardIdentity {
13 #[serde(default, skip_serializing_if = "String::is_empty")]
14 pub id: String,
15 pub name: String,
16 pub set_code: String,
17 pub card_number: String,
18 #[serde(default, skip_serializing_if = "Option::is_none")]
19 #[ts(optional)]
20 pub oracle_id: Option<String>,
21 #[serde(default, skip_serializing_if = "Option::is_none")]
22 #[ts(optional)]
23 pub token_script: Option<TokenScript>,
24 #[serde(default, skip_serializing_if = "Option::is_none")]
25 #[ts(optional)]
26 pub foil: Option<bool>,
27}
28
29#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
33#[serde(rename_all = "camelCase")]
34#[ts(export, export_to = "deck/index.ts")]
35pub struct CardRulesSummary {
36 #[serde(default)]
37 pub color: String,
38 #[serde(default)]
39 pub color_identity: Vec<String>,
40 #[serde(default)]
41 pub mana_cost: String,
42 #[serde(default)]
43 pub cmc: f32,
44 #[serde(default)]
45 pub types: Vec<String>,
46 #[serde(default)]
47 pub subtypes: Vec<String>,
48 #[serde(default)]
49 pub supertypes: Vec<String>,
50 #[serde(default, skip_serializing_if = "Option::is_none")]
51 #[ts(optional)]
52 pub keywords: Option<Vec<String>>,
53 #[serde(default, skip_serializing_if = "Option::is_none")]
54 #[ts(optional)]
55 pub power: Option<String>,
56 #[serde(default, skip_serializing_if = "Option::is_none")]
57 #[ts(optional)]
58 pub toughness: Option<String>,
59 #[serde(default)]
60 pub text: String,
61 #[serde(default, skip_serializing_if = "Option::is_none")]
62 #[ts(optional)]
63 pub layout: Option<String>,
64 #[serde(default, skip_serializing_if = "Option::is_none")]
65 #[ts(optional)]
66 pub is_double_faced: Option<bool>,
67 #[serde(default, skip_serializing_if = "Option::is_none")]
70 #[ts(optional)]
71 pub back_face: Option<CardBackFaceSummary>,
72}
73
74#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
75#[serde(rename_all = "camelCase")]
76#[ts(export, export_to = "deck/index.ts")]
77pub struct CardBackFaceSummary {
78 pub name: String,
79 #[serde(default)]
80 pub mana_cost: String,
81 #[serde(default)]
82 pub type_line: String,
83 #[serde(default)]
84 pub oracle_text: String,
85 #[serde(default)]
86 pub uris: CardImageUris,
87}
88
89#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
90#[ts(export, export_to = "deck/index.ts")]
91pub struct CardImageUris {
92 #[serde(default)]
93 pub small: String,
94 #[serde(default)]
95 pub normal: String,
96 #[serde(default)]
97 pub large: String,
98 #[serde(default)]
99 pub png: String,
100 #[serde(default)]
101 pub art_crop: String,
102 #[serde(default)]
103 pub border_crop: String,
104}
105
106#[derive(Debug, Clone, Copy, Serialize, Deserialize, TS)]
107#[serde(rename_all = "snake_case")]
108#[ts(export, export_to = "deck/index.ts")]
109pub enum CardPartComponent {
110 Token,
111 ComboPiece,
112 MeldPart,
113 MeldResult,
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize, TS)]
117#[serde(rename_all = "camelCase")]
118#[ts(export, export_to = "deck/index.ts")]
119pub struct CardPart {
120 pub name: String,
121 pub component: CardPartComponent,
122}
123
124#[derive(Debug, Clone, Default, Serialize, TS)]
125#[serde(rename_all = "camelCase")]
126#[ts(export, export_to = "deck/index.ts")]
127pub struct DeckCard {
128 pub identity: DeckCardIdentity,
129 #[serde(flatten)]
130 #[ts(flatten)]
131 pub rules: CardRulesSummary,
132 #[serde(default)]
133 pub uris: CardImageUris,
134 #[serde(default, skip_serializing_if = "Option::is_none")]
135 #[ts(optional)]
136 pub all_parts: Option<Vec<CardPart>>,
137}
138
139pub const OUTDATED_CLIENT_MESSAGE: &str =
140 "this app version is out of date — download the latest release at manabrew.app to play online";
141
142#[derive(Deserialize)]
147#[serde(rename_all = "camelCase")]
148struct DeckCardWire {
149 #[serde(default)]
150 identity: Option<DeckCardIdentity>,
151 #[serde(default)]
152 name: Option<String>,
153 #[serde(flatten)]
154 rules: CardRulesSummary,
155 #[serde(default)]
156 uris: CardImageUris,
157 #[serde(default)]
158 all_parts: Option<Vec<CardPart>>,
159}
160
161impl<'de> Deserialize<'de> for DeckCard {
162 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
163 let wire = DeckCardWire::deserialize(deserializer)?;
164 let identity = match wire.identity {
165 Some(identity) => identity,
166 None if wire.name.is_some() => {
167 return Err(serde::de::Error::custom(OUTDATED_CLIENT_MESSAGE));
168 }
169 None => return Err(serde::de::Error::missing_field("identity")),
170 };
171 Ok(DeckCard {
172 identity,
173 rules: wire.rules,
174 uris: wire.uris,
175 all_parts: wire.all_parts,
176 })
177 }
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize, TS)]
181#[serde(rename_all = "camelCase")]
182#[ts(export, export_to = "deck/index.ts")]
183pub struct DeckLabel {
184 pub name: String,
185 #[serde(default, skip_serializing_if = "Option::is_none")]
186 #[ts(optional)]
187 pub color: Option<String>,
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize, TS)]
191#[serde(rename_all = "camelCase")]
192#[ts(export, export_to = "deck/index.ts")]
193pub struct DeckEditorTag {
194 pub id: String,
195 pub name: String,
196 #[serde(default, skip_serializing_if = "Option::is_none")]
197 #[ts(optional)]
198 pub color: Option<String>,
199 #[serde(default, skip_serializing_if = "Option::is_none")]
200 #[ts(optional)]
201 pub icon: Option<String>,
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize, TS)]
205#[serde(rename_all = "camelCase")]
206#[ts(export, export_to = "deck/index.ts")]
207pub struct DeckEditorGroup {
208 pub id: String,
209 pub name: String,
210 pub card_names: Vec<String>,
211 #[serde(default, skip_serializing_if = "Option::is_none")]
212 #[ts(optional)]
213 pub collapsed: Option<bool>,
214 #[serde(default, skip_serializing_if = "Option::is_none")]
215 #[ts(optional)]
216 pub pinned: Option<bool>,
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize, TS)]
220#[serde(rename_all = "camelCase")]
221#[ts(export, export_to = "deck/index.ts")]
222pub enum DeckEditorGroupBy {
223 Type,
224 Cmc,
225 Color,
226 Custom,
227}
228
229#[derive(Debug, Clone, Serialize, Deserialize, TS)]
230#[ts(export, export_to = "deck/index.ts")]
231pub enum DeckEditorSortBy {
232 #[serde(rename = "name")]
233 Name,
234 #[serde(rename = "mana-value")]
235 ManaValue,
236 #[serde(rename = "quantity")]
237 Quantity,
238 #[serde(rename = "owned")]
239 Owned,
240 #[serde(rename = "not-owned")]
241 NotOwned,
242}
243
244#[derive(Debug, Clone, Serialize, Deserialize, TS)]
245#[serde(rename_all = "camelCase")]
246#[ts(export, export_to = "deck/index.ts")]
247pub enum DeckEditorViewMode {
248 List,
249 Visual,
250 Stack,
251}
252
253#[derive(Debug, Clone, Serialize, Deserialize, TS)]
254#[serde(rename_all = "camelCase")]
255#[ts(export, export_to = "deck/index.ts")]
256pub enum DeckEditorCollectionFilter {
257 All,
258 Exact,
259 Other,
260 Partial,
261 Missing,
262}
263
264#[derive(Debug, Clone, Serialize, Deserialize, TS)]
265#[serde(rename_all = "camelCase")]
266#[ts(export, export_to = "deck/index.ts")]
267pub enum DeckEditorDestination {
268 Main,
269 Side,
270 Maybe,
271}
272
273#[derive(Debug, Clone, Serialize, Deserialize, TS)]
274#[serde(rename_all = "camelCase")]
275#[ts(export, export_to = "deck/index.ts")]
276pub struct DeckEditorLayout {
277 pub id: String,
278 pub name: String,
279 pub group_by: DeckEditorGroupBy,
280 pub sort_by: DeckEditorSortBy,
281 pub groups: Vec<DeckEditorGroup>,
282 #[serde(default, skip_serializing_if = "Option::is_none")]
283 #[ts(optional)]
284 pub filter: Option<String>,
285 #[serde(default, skip_serializing_if = "Option::is_none")]
286 #[ts(optional)]
287 pub card_size: Option<u32>,
288 #[serde(default, skip_serializing_if = "Option::is_none")]
289 #[ts(optional)]
290 pub view_mode: Option<DeckEditorViewMode>,
291 #[serde(default, skip_serializing_if = "Option::is_none")]
292 #[ts(optional)]
293 pub collection_filter: Option<DeckEditorCollectionFilter>,
294 #[serde(default, skip_serializing_if = "Option::is_none")]
295 #[ts(optional)]
296 pub default_destination: Option<DeckEditorDestination>,
297}
298
299#[derive(Debug, Clone, Serialize, Deserialize, TS)]
300#[serde(rename_all = "camelCase")]
301#[ts(export, export_to = "deck/index.ts")]
302pub enum DeckPriceProvider {
303 Tcgplayer,
304 Cardmarket,
305 Cardhoarder,
306}
307
308#[derive(Debug, Clone, Serialize, Deserialize, TS)]
309#[serde(rename_all = "camelCase")]
310#[ts(export, export_to = "deck/index.ts")]
311pub enum DeckAcquisitionStatus {
312 Ordered,
313 Proxy,
314}
315
316#[derive(Debug, Clone, Serialize, Deserialize, TS)]
317#[serde(rename_all = "camelCase")]
318#[ts(export, export_to = "deck/index.ts")]
319pub struct DeckSideboardPlan {
320 pub id: String,
321 pub matchup: String,
322 pub bring_in: String,
323 pub take_out: String,
324 pub notes: String,
325}
326
327#[derive(Debug, Clone, Serialize, Deserialize, TS)]
328#[serde(rename_all = "camelCase")]
329#[ts(export, export_to = "deck/index.ts")]
330pub struct DeckEditorGoals {
331 #[serde(default, skip_serializing_if = "Option::is_none")]
332 #[ts(optional)]
333 pub min_lands: Option<u32>,
334 #[serde(default, skip_serializing_if = "Option::is_none")]
335 #[ts(optional)]
336 pub max_lands: Option<u32>,
337 #[serde(default, skip_serializing_if = "Option::is_none")]
338 #[ts(optional)]
339 pub max_missing_cards: Option<u32>,
340 #[serde(default, skip_serializing_if = "Option::is_none")]
341 #[ts(optional)]
342 pub max_average_mana_value: Option<f64>,
343 #[serde(default, skip_serializing_if = "Option::is_none")]
344 #[ts(optional, type = "Record<string, number>")]
345 pub tag_targets: Option<BTreeMap<String, u32>>,
346}
347
348#[derive(Debug, Clone, Serialize, Deserialize, TS)]
349#[serde(rename_all = "camelCase")]
350#[ts(export, export_to = "deck/index.ts")]
351pub struct DeckEditorMetadata {
352 pub version: u32,
353 #[serde(default)]
354 pub tags: Vec<DeckEditorTag>,
355 #[serde(default)]
356 pub layouts: Vec<DeckEditorLayout>,
357 #[serde(default, skip_serializing_if = "Option::is_none")]
358 #[ts(optional)]
359 pub active_layout_id: Option<String>,
360 #[serde(default, skip_serializing_if = "Option::is_none")]
361 #[ts(optional)]
362 pub sideboard_plans: Option<Vec<DeckSideboardPlan>>,
363 #[serde(default, skip_serializing_if = "Option::is_none")]
364 #[ts(optional)]
365 pub budget_usd: Option<f64>,
366 #[serde(default, skip_serializing_if = "Option::is_none")]
367 #[ts(optional)]
368 pub budget_amount: Option<f64>,
369 #[serde(default, skip_serializing_if = "Option::is_none")]
370 #[ts(optional)]
371 pub price_provider: Option<DeckPriceProvider>,
372 #[serde(default, skip_serializing_if = "Option::is_none")]
373 #[ts(optional)]
374 pub goals: Option<DeckEditorGoals>,
375 #[serde(default, skip_serializing_if = "Option::is_none")]
376 #[ts(optional)]
377 pub dismissed_hints: Option<Vec<String>>,
378 #[serde(default, skip_serializing_if = "Option::is_none")]
379 #[ts(optional, type = "Record<string, \"ordered\" | \"proxy\">")]
380 pub acquisition: Option<BTreeMap<String, DeckAcquisitionStatus>>,
381}
382
383#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
384#[serde(rename_all = "camelCase")]
385#[ts(export, export_to = "deck/index.ts")]
386pub enum DeckFormat {
387 Standard,
388 Pioneer,
389 Modern,
390 Legacy,
391 Vintage,
392 Pauper,
393 Commander,
394 Brawl,
395 Oathbreaker,
396 Draft,
397 Sealed,
398}
399
400#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
401#[serde(rename_all = "camelCase")]
402#[ts(export, export_to = "deck/index.ts")]
403pub struct Deck {
404 #[serde(default, skip_serializing_if = "Option::is_none")]
405 #[ts(optional)]
406 pub version: Option<String>,
407 #[serde(default, skip_serializing_if = "Option::is_none")]
408 #[ts(optional)]
409 pub id: Option<String>,
410 pub name: String,
411 #[serde(default, skip_serializing_if = "Option::is_none")]
412 #[ts(optional)]
413 pub description: Option<String>,
414 #[serde(default, skip_serializing_if = "Option::is_none")]
415 #[ts(optional)]
416 pub color: Option<String>,
417 #[serde(default, skip_serializing_if = "Option::is_none")]
418 #[ts(optional)]
419 pub format: Option<DeckFormat>,
420 #[serde(default)]
421 pub cards: Vec<DeckCard>,
422 #[serde(default)]
423 pub sideboard: Vec<DeckCard>,
424 #[serde(default, skip_serializing_if = "Option::is_none")]
425 #[ts(optional)]
426 pub attractions: Option<Vec<DeckCard>>,
427 #[serde(default, skip_serializing_if = "Option::is_none")]
428 #[ts(optional)]
429 pub contraptions: Option<Vec<DeckCard>>,
430 #[serde(default, skip_serializing_if = "Option::is_none")]
431 #[ts(optional)]
432 pub schemes: Option<Vec<DeckCard>>,
433 #[serde(default, skip_serializing_if = "Option::is_none")]
434 #[ts(optional)]
435 pub planes: Option<Vec<DeckCard>>,
436 #[serde(default, skip_serializing_if = "Option::is_none")]
437 #[ts(optional)]
438 pub commanders: Option<Vec<DeckCard>>,
439 #[serde(default, skip_serializing_if = "Option::is_none")]
440 #[ts(optional)]
441 pub companion: Option<DeckCard>,
442 #[serde(default, skip_serializing_if = "Option::is_none")]
443 #[ts(optional)]
444 pub maybeboard: Option<Vec<DeckCard>>,
445 #[serde(default, skip_serializing_if = "Option::is_none")]
446 #[ts(optional)]
447 pub draft: Option<bool>,
448 #[serde(default, skip_serializing_if = "Option::is_none")]
449 #[ts(optional)]
450 pub labels: Option<Vec<DeckLabel>>,
451 #[serde(default, skip_serializing_if = "Option::is_none")]
452 #[ts(optional)]
453 pub custom_tags: Option<Vec<String>>,
454 #[serde(default, skip_serializing_if = "Option::is_none")]
455 #[ts(optional, type = "Record<string, Array<string>>")]
456 pub card_tags: Option<BTreeMap<String, Vec<String>>>,
457 #[serde(default, skip_serializing_if = "Option::is_none")]
458 #[ts(optional)]
459 pub editor: Option<DeckEditorMetadata>,
460 #[serde(default, skip_serializing_if = "Option::is_none")]
461 #[ts(optional)]
462 pub cover_card_name: Option<String>,
463 #[serde(default, skip_serializing_if = "Option::is_none")]
464 #[ts(optional)]
465 pub cover_card_face: Option<u8>,
466 #[serde(default, skip_serializing_if = "Option::is_none")]
467 #[ts(optional)]
468 pub playmat: Option<String>,
469 #[serde(default, skip_serializing_if = "Option::is_none")]
470 #[ts(optional)]
471 pub playmat_settings: Option<PlaymatSettings>,
472 #[serde(default, skip_serializing_if = "Option::is_none")]
473 #[ts(optional, type = "Record<string, { x: number; y: number }>")]
474 pub stack_positions: Option<serde_json::Value>,
475 #[serde(default, skip_serializing_if = "Option::is_none")]
476 #[ts(optional)]
477 pub tokens: Option<Vec<DeckCard>>,
478}
479
480pub fn deck_fingerprint(deck: &Deck) -> String {
481 let mut hasher = Sha256::new();
482 update_fingerprint(&mut hasher, &deck.name);
483 update_fingerprint(
484 &mut hasher,
485 &serde_json::to_string(&deck.format).unwrap_or_default(),
486 );
487 let mut cards = Vec::new();
488 cards.extend(deck.cards.iter().map(|card| ("main", card)));
489 cards.extend(deck.sideboard.iter().map(|card| ("sideboard", card)));
490 cards.extend(
491 deck.attractions
492 .iter()
493 .flatten()
494 .map(|card| ("attraction", card)),
495 );
496 cards.extend(
497 deck.contraptions
498 .iter()
499 .flatten()
500 .map(|card| ("contraption", card)),
501 );
502 cards.extend(deck.schemes.iter().flatten().map(|card| ("scheme", card)));
503 cards.extend(deck.planes.iter().flatten().map(|card| ("plane", card)));
504 cards.extend(
505 deck.commanders
506 .iter()
507 .flatten()
508 .map(|card| ("commander", card)),
509 );
510 cards.extend(deck.companion.iter().map(|card| ("companion", card)));
511 cards.sort_by(|(left_section, left), (right_section, right)| {
512 (
513 left_section,
514 &left.identity.name,
515 &left.identity.set_code,
516 &left.identity.card_number,
517 )
518 .cmp(&(
519 right_section,
520 &right.identity.name,
521 &right.identity.set_code,
522 &right.identity.card_number,
523 ))
524 });
525 for (section, card) in cards {
526 update_fingerprint(&mut hasher, section);
527 update_fingerprint(&mut hasher, &card.identity.name);
528 update_fingerprint(&mut hasher, &card.identity.set_code);
529 update_fingerprint(&mut hasher, &card.identity.card_number);
530 }
531 let digest = hasher.finalize();
532 let mut fingerprint = String::with_capacity(digest.len() * 2);
533 for byte in digest {
534 let _ = write!(fingerprint, "{byte:02x}");
535 }
536 fingerprint
537}
538
539fn update_fingerprint(hasher: &mut Sha256, value: &str) {
540 hasher.update((value.len() as u64).to_le_bytes());
541 hasher.update(value.as_bytes());
542}