1use std::collections::{BTreeMap, HashMap};
2
3use chrono::{DateTime, Utc};
4use serde::{
5 de::{self, MapAccess, Visitor},
6 Deserialize, Deserializer,
7};
8
9use torn_api_macros::ApiCategory;
10
11use crate::{de_util, user};
12
13#[derive(Debug, Clone, Copy, ApiCategory)]
14#[api(category = "torn")]
15#[non_exhaustive]
16pub enum TornSelection {
17 #[api(
18 field = "competition",
19 with = "decode_competition",
20 type = "Option<Competition>"
21 )]
22 Competition,
23
24 #[api(
25 type = "HashMap<String, TerritoryWar>",
26 with = "decode_territory_wars",
27 field = "territorywars"
28 )]
29 TerritoryWars,
30
31 #[api(type = "HashMap<String, Racket>", field = "rackets")]
32 Rackets,
33
34 #[api(
35 type = "HashMap<String, Territory>",
36 with = "decode_territory",
37 field = "territory"
38 )]
39 Territory,
40
41 #[api(type = "TerritoryWarReport", field = "territorywarreport")]
42 TerritoryWarReport,
43
44 #[api(type = "BTreeMap<i32, Item>", field = "items")]
45 Items,
46}
47
48pub type Selection = TornSelection;
49
50#[derive(Debug, Clone, Deserialize)]
51pub struct EliminationLeaderboard {
52 pub position: i16,
53 pub team: user::EliminationTeam,
54 pub score: i16,
55 pub lives: i16,
56 pub participants: Option<i16>,
57 pub wins: Option<i32>,
58 pub losses: Option<i32>,
59}
60
61#[derive(Debug, Clone)]
62pub enum Competition {
63 Elimination { teams: Vec<EliminationLeaderboard> },
64 Unkown(String),
65}
66
67fn decode_territory_wars<'de, D>(deserializer: D) -> Result<HashMap<String, TerritoryWar>, D::Error>
68where
69 D: serde::Deserializer<'de>,
70{
71 let map: Option<_> = serde::Deserialize::deserialize(deserializer)?;
72
73 Ok(map.unwrap_or_default())
74}
75
76fn decode_competition<'de, D>(deserializer: D) -> Result<Option<Competition>, D::Error>
77where
78 D: serde::Deserializer<'de>,
79{
80 struct CompetitionVisitor;
81
82 impl<'de> Visitor<'de> for CompetitionVisitor {
83 type Value = Option<Competition>;
84
85 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
86 formatter.write_str("struct Competition")
87 }
88
89 fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
90 where
91 D: serde::Deserializer<'de>,
92 {
93 deserializer.deserialize_map(self)
94 }
95
96 fn visit_none<E>(self) -> Result<Self::Value, E>
97 where
98 E: de::Error,
99 {
100 Ok(None)
101 }
102
103 fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error>
104 where
105 V: MapAccess<'de>,
106 {
107 let mut name = None;
108 let mut teams = None;
109
110 while let Some(key) = map.next_key()? {
111 match key {
112 "name" => {
113 name = Some(map.next_value()?);
114 }
115 "teams" => {
116 teams = Some(map.next_value()?);
117 }
118 _ => (),
119 };
120 }
121
122 let name = name.ok_or_else(|| de::Error::missing_field("name"))?;
123
124 match name {
125 "Elimination" => Ok(Some(Competition::Elimination {
126 teams: teams.ok_or_else(|| de::Error::missing_field("teams"))?,
127 })),
128 "" => Ok(None),
129 v => Ok(Some(Competition::Unkown(v.to_owned()))),
130 }
131 }
132 }
133
134 deserializer.deserialize_option(CompetitionVisitor)
135}
136
137#[derive(Debug, Clone, Deserialize)]
138pub struct TerritoryWar {
139 pub territory_war_id: i32,
140 pub assaulting_faction: i32,
141 pub defending_faction: i32,
142
143 #[serde(with = "chrono::serde::ts_seconds")]
144 pub started: DateTime<Utc>,
145 #[serde(with = "chrono::serde::ts_seconds")]
146 pub ends: DateTime<Utc>,
147}
148
149#[derive(Debug, Clone, Deserialize)]
150pub struct Racket {
151 pub name: String,
152 pub level: i16,
153 pub reward: String,
154
155 #[serde(with = "chrono::serde::ts_seconds")]
156 pub created: DateTime<Utc>,
157 #[serde(with = "chrono::serde::ts_seconds")]
158 pub changed: DateTime<Utc>,
159
160 #[serde(rename = "faction")]
161 pub faction_id: Option<i32>,
162}
163
164#[derive(Debug, Clone, Deserialize)]
165pub struct Territory {
166 pub sector: i16,
167 pub size: i16,
168 pub slots: i16,
169 pub daily_respect: i16,
170 pub faction: i32,
171
172 pub neighbors: Vec<String>,
173 pub war: Option<TerritoryWar>,
174 pub racket: Option<Racket>,
175}
176
177fn decode_territory<'de, D>(deserializer: D) -> Result<HashMap<String, Territory>, D::Error>
178where
179 D: Deserializer<'de>,
180{
181 Ok(Option::deserialize(deserializer)?.unwrap_or_default())
182}
183
184#[derive(Clone, Debug, Deserialize)]
185pub struct TerritoryWarReportTerritory {
186 pub name: String,
187}
188
189#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
190#[serde(rename_all = "snake_case")]
191pub enum TerritoryWarOutcome {
192 EndWithPeaceTreaty,
193 EndWithDestroyDefense,
194 FailAssault,
195 SuccessAssault,
196}
197
198#[derive(Clone, Debug, Deserialize)]
199pub struct TerritoryWarReportWar {
200 #[serde(with = "chrono::serde::ts_seconds")]
201 pub start: DateTime<Utc>,
202 #[serde(with = "chrono::serde::ts_seconds")]
203 pub end: DateTime<Utc>,
204
205 pub result: TerritoryWarOutcome,
206}
207
208#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq, Hash)]
209#[serde(rename_all = "snake_case")]
210pub enum TerritoryWarReportRole {
211 Aggressor,
212 Defender,
213}
214
215#[derive(Debug, Clone, Deserialize)]
216pub struct TerritoryWarReportFaction {
217 pub name: String,
218 pub score: i32,
219 pub joins: i32,
220 pub clears: i32,
221 #[serde(rename = "type")]
222 pub role: TerritoryWarReportRole,
223}
224
225#[derive(Clone, Debug, Deserialize)]
226pub struct TerritoryWarReport {
227 pub territory: TerritoryWarReportTerritory,
228 pub war: TerritoryWarReportWar,
229 pub factions: HashMap<i32, TerritoryWarReportFaction>,
230}
231
232#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq, Hash)]
233#[non_exhaustive]
234pub enum ItemType {
235 Primary,
236 Secondary,
237 Melee,
238 Temporary,
239 Defensive,
240 Collectible,
241 Medical,
242 Drug,
243 Booster,
244 #[serde(rename = "Energy Drink")]
245 EnergyDrink,
246 Alcohol,
247 Book,
248 Candy,
249 Car,
250 Clothing,
251 Electronic,
252 Enhancer,
253 Flower,
254 Jewelry,
255 Other,
256 Special,
257 #[serde(rename = "Supply Pack")]
258 SupplyPack,
259 Virus,
260}
261
262#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq, Hash)]
263#[non_exhaustive]
264pub enum WeaponType {
266 Slashing,
267 Rifle,
268 SMG,
269 Piercing,
270 Clubbing,
271 Pistol,
272 #[serde(rename = "Machine gun")]
273 MachineGun,
274 Mechanical,
275 Temporary,
276 Heavy,
277 Shotgun,
278}
279
280#[derive(Debug, Clone, Deserialize)]
281pub struct Item<'a> {
282 pub name: String,
283 pub description: String,
284 #[serde(deserialize_with = "de_util::empty_string_is_none")]
285 pub effect: Option<&'a str>,
286 #[serde(deserialize_with = "de_util::empty_string_is_none")]
287 pub requirement: Option<&'a str>,
288 #[serde(rename = "type")]
289 pub item_type: ItemType,
290 pub weapon_type: Option<WeaponType>,
291 #[serde(deserialize_with = "de_util::zero_is_none")]
292 pub buy_price: Option<u64>,
293 #[serde(deserialize_with = "de_util::zero_is_none")]
294 pub sell_price: Option<u64>,
295 #[serde(deserialize_with = "de_util::zero_is_none")]
296 pub market_value: Option<u64>,
297 #[serde(deserialize_with = "de_util::zero_is_none")]
298 pub circulation: Option<u32>,
299 pub image: String,
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305 use crate::tests::{async_test, setup, Client, ClientTrait};
306
307 #[async_test]
308 async fn competition() {
309 let key = setup();
310
311 let response = Client::default()
312 .torn_api(key)
313 .torn(|b| {
314 b.selections([
315 TornSelection::Competition,
316 TornSelection::TerritoryWars,
317 TornSelection::Rackets,
318 ])
319 })
320 .await
321 .unwrap();
322
323 response.competition().unwrap();
324 response.territory_wars().unwrap();
325 response.rackets().unwrap();
326 }
327
328 #[async_test]
329 async fn territory() {
330 let key = setup();
331
332 let response = Client::default()
333 .torn_api(key)
334 .torn(|b| b.selections([Selection::Territory]).id("NSC"))
335 .await
336 .unwrap();
337
338 let territory = response.territory().unwrap();
339 assert!(territory.contains_key("NSC"));
340 }
341
342 #[async_test]
343 async fn invalid_territory() {
344 let key = setup();
345
346 let response = Client::default()
347 .torn_api(key)
348 .torn(|b| b.selections([Selection::Territory]).id("AAA"))
349 .await
350 .unwrap();
351
352 assert!(response.territory().unwrap().is_empty());
353 }
354
355 #[async_test]
356 async fn territory_war_report() {
357 let key = setup();
358
359 let response = Client::default()
360 .torn_api(&key)
361 .torn(|b| b.selections([Selection::TerritoryWarReport]).id(37403))
362 .await
363 .unwrap();
364
365 assert_eq!(
366 response.territory_war_report().unwrap().war.result,
367 TerritoryWarOutcome::SuccessAssault
368 );
369
370 let response = Client::default()
371 .torn_api(&key)
372 .torn(|b| b.selections([Selection::TerritoryWarReport]).id(37502))
373 .await
374 .unwrap();
375
376 assert_eq!(
377 response.territory_war_report().unwrap().war.result,
378 TerritoryWarOutcome::FailAssault
379 );
380
381 let response = Client::default()
382 .torn_api(&key)
383 .torn(|b| b.selections([Selection::TerritoryWarReport]).id(37860))
384 .await
385 .unwrap();
386
387 assert_eq!(
388 response.territory_war_report().unwrap().war.result,
389 TerritoryWarOutcome::EndWithPeaceTreaty
390 );
391
392 let response = Client::default()
393 .torn_api(&key)
394 .torn(|b| b.selections([Selection::TerritoryWarReport]).id(23757))
395 .await
396 .unwrap();
397
398 assert_eq!(
399 response.territory_war_report().unwrap().war.result,
400 TerritoryWarOutcome::EndWithDestroyDefense
401 );
402 }
403
404 #[async_test]
405 async fn item() {
406 let key = setup();
407
408 let response = Client::default()
409 .torn_api(key)
410 .torn(|b| b.selections([Selection::Items]).id(837))
411 .await
412 .unwrap();
413
414 let item_list = response.items().unwrap();
415 assert!(item_list.contains_key(&837));
416 }
417}