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