Skip to main content

ddapi_rs/scheme/ddnet/
master.rs

1use crate::prelude::Addr;
2use crate::prelude::{addr_serialization, Protocol};
3use crate::scheme::DDNET_BASE_URL;
4use serde::{Deserialize, Serialize};
5use std::collections::{HashMap, HashSet};
6
7fn default_location() -> String {
8    "unknown".to_string()
9}
10
11/// The API sends `country` as a string on some servers and as an integer on
12/// others; accept both and normalize to a string.
13fn deserialize_optional_string<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
14where
15    D: serde::Deserializer<'de>,
16{
17    use serde::de::Error;
18    let value = Option::<serde_json::Value>::deserialize(deserializer)?;
19    Ok(match value {
20        None => None,
21        Some(serde_json::Value::String(s)) => Some(s),
22        Some(serde_json::Value::Number(n)) => Some(n.to_string()),
23        Some(other) => {
24            return Err(D::Error::custom(format!(
25                "expected string or number for country, got `{other}`"
26            )))
27        }
28    })
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
32pub enum MasterServer {
33    One = 1,
34    Two = 2,
35    Three = 3,
36    Four = 4,
37}
38
39impl MasterServer {
40    #[must_use]
41    pub fn get_index(&self) -> i32 {
42        *self as i32
43    }
44
45    #[must_use]
46    pub fn api(&self) -> String {
47        format!(
48            "https://master{}.{}/ddnet/15/servers.json",
49            self.get_index(),
50            DDNET_BASE_URL
51        )
52    }
53}
54
55#[derive(Default, Debug, Clone)]
56pub struct ClanCount {
57    pub name: String,
58    pub count: usize,
59}
60
61#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
62#[serde(rename_all = "camelCase")]
63pub struct Master {
64    pub communities: Vec<Community>,
65    pub servers: Vec<Server>,
66}
67
68#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
69pub struct Community {
70    pub id: String,
71    pub name: String,
72    pub has_finishes: bool,
73    pub icon: Icon,
74    pub contact_urls: Vec<String>,
75}
76
77#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
78pub struct Icon {
79    pub sha256: String,
80    pub url: String,
81}
82
83impl Master {
84    #[must_use]
85    pub fn api(master: MasterServer) -> String {
86        master.api()
87    }
88
89    #[must_use]
90    pub fn count_clients(&self) -> usize {
91        self.servers.iter().map(|s| s.info.clients.len()).sum()
92    }
93
94    #[must_use]
95    pub fn get_clans(&self) -> Vec<ClanCount> {
96        self.get_filtered_clans(None)
97    }
98
99    #[must_use]
100    pub fn get_filtered_clans(&self, filters: Option<Vec<&str>>) -> Vec<ClanCount> {
101        if self.servers.is_empty() {
102            return Vec::new();
103        }
104
105        let filter_set: HashSet<&str> = filters.unwrap_or_default().into_iter().collect();
106
107        let mut clan_counts = HashMap::new();
108
109        for server in &self.servers {
110            for client in &server.info.clients {
111                if !client.clan.is_empty() && !filter_set.contains(client.clan.as_str()) {
112                    *clan_counts.entry(client.clan.clone()).or_insert(0) += 1;
113                }
114            }
115        }
116
117        let mut result: Vec<ClanCount> = clan_counts
118            .into_iter()
119            .map(|(name, count)| ClanCount { name, count })
120            .collect();
121
122        result.sort_by_key(|x| std::cmp::Reverse(x.count));
123        result
124    }
125}
126
127#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
128pub struct Server {
129    #[serde(with = "addr_serialization")]
130    pub addresses: Vec<Addr>,
131    pub community: Option<String>,
132    #[serde(default = "default_location")]
133    pub location: String,
134    pub info: Info,
135}
136
137impl Server {
138    #[must_use]
139    pub fn count_client(&self) -> usize {
140        self.info.clients.len()
141    }
142
143    #[must_use]
144    pub fn ipv4_addresses(&self) -> Vec<&Addr> {
145        self.addresses
146            .iter()
147            .filter(|addr| addr.ip.is_ipv4())
148            .collect()
149    }
150
151    #[must_use]
152    pub fn ipv6_addresses(&self) -> Vec<&Addr> {
153        self.addresses
154            .iter()
155            .filter(|addr| addr.ip.is_ipv6())
156            .collect()
157    }
158
159    #[must_use]
160    pub fn addresses_by_protocol(&self, protocol: Protocol) -> Vec<&Addr> {
161        self.addresses
162            .iter()
163            .filter(|addr| addr.protocol == protocol)
164            .collect()
165    }
166}
167
168#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
169pub struct Info {
170    pub max_clients: i64,
171    pub max_players: i64,
172    #[serde(default)]
173    pub passworded: bool,
174    #[serde(rename = "game_type")]
175    pub gametype: String,
176    pub name: String,
177    pub map: IMap,
178    pub version: String,
179    #[serde(default)]
180    pub clients: Vec<Client>,
181    #[serde(default)]
182    pub requires_login: bool,
183    #[serde(default)]
184    pub client_score_kind: Option<String>,
185    #[serde(default, deserialize_with = "deserialize_optional_string")]
186    pub country: Option<String>,
187    #[serde(default)]
188    pub flags: Option<Vec<String>>,
189    #[serde(default)]
190    pub flag: Option<i64>,
191    #[serde(default)]
192    pub identity_key: Option<String>,
193}
194
195#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
196pub struct IMap {
197    pub name: String,
198    pub sha256: Option<String>,
199    pub size: Option<i64>,
200    #[serde(default)]
201    pub url: Option<String>,
202    #[serde(default, deserialize_with = "deserialize_optional_string")]
203    pub tw_crc: Option<String>,
204}
205
206#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
207pub struct Client {
208    pub name: String,
209    pub clan: String,
210    pub country: i32,
211    pub score: i64,
212    #[serde(default)]
213    pub is_player: bool,
214    pub skin: Option<Skin>,
215    #[serde(default)]
216    pub afk: bool,
217    #[serde(default)]
218    pub team: i64,
219}
220
221#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
222pub struct Skin {
223    pub name: Option<String>,
224    pub color_body: Option<i64>,
225    pub color_feet: Option<i64>,
226    #[serde(default)]
227    pub body: Option<SkinPart>,
228    #[serde(default)]
229    pub marking: Option<SkinPart>,
230    #[serde(default)]
231    pub decoration: Option<SkinPart>,
232    #[serde(default)]
233    pub eyes: Option<SkinPart>,
234    #[serde(default)]
235    pub feet: Option<SkinPart>,
236    #[serde(default)]
237    pub hands: Option<SkinPart>,
238}
239
240#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
241pub struct SkinPart {
242    pub name: String,
243    #[serde(default)]
244    pub color: Option<i64>,
245}