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