1use std::convert::TryFrom;
32use std::fmt::{self, Debug};
33use std::fmt::Formatter;
34
35use enum_primitive::{enum_from_primitive, enum_from_primitive_impl, enum_from_primitive_impl_ty, FromPrimitive};
36use lazy_static::lazy_static;
37use regex::Regex;
38use serde::de::{self, Deserialize, Deserializer, Visitor};
39use serde_derive::Serialize;
40use thiserror::Error;
41
42#[derive(Clone, Copy, PartialEq, Eq, Hash, Default, Serialize)]
43pub struct SteamID(u64);
44
45impl SteamID {
46 pub fn account_id(&self) -> u32 {
47 (self.0 & 0xFFFFFFFF) as u32
49 }
50
51 pub fn set_account_id(&mut self, account_id: u32) {
52 self.0 &= 0xFFFFFFFF00000000;
53 self.0 |= u64::from(account_id);
54 }
55
56 pub fn instance(&self) -> Instance {
57 Instance::from_u64((self.0 >> 32) & 0xFFFFF).unwrap_or(Instance::Invalid)
58 }
59
60 pub fn set_instance(&mut self, instance: Instance) {
61 self.0 &= 0xFFF00000FFFFFFFF;
62 self.0 |= (instance as u64) << 32;
63 }
64
65 pub fn account_type(&self) -> AccountType {
66 AccountType::from_u64((self.0 >> 52) & 0xF).unwrap_or(AccountType::Invalid)
67 }
68
69 pub fn set_account_type(&mut self, account_type: AccountType) {
70 self.0 &= 0xFF0FFFFFFFFFFFFF;
71 self.0 |= (account_type as u64) << 52;
72 }
73
74 pub fn universe(&self) -> Universe {
75 Universe::from_u64((self.0 >> 56) & 0xFF).unwrap_or(Universe::Invalid)
76 }
77
78 pub fn set_universe(&mut self, universe: Universe) {
79 self.0 &= 0x00FFFFFFFFFFFFFF;
80 self.0 |= (universe as u64) << 56;
81 }
82
83 pub fn new(
84 account_id: u32,
85 instance: Instance,
86 account_type: AccountType,
87 universe: Universe,
88 ) -> Self {
89 #[cfg_attr(rustfmt, rustfmt_skip)]
90 Self::from(
91 u64::from(account_id) | ((instance as u64) << 32) |
92 ((account_type as u64) << 52) | ((universe as u64) << 56),
93 )
94 }
95
96 pub fn steam2(&self) -> String {
97 match self.account_type() {
98 AccountType::Individual | AccountType::Invalid => {
99 let id = self.account_id();
100 format!("STEAM_{}:{}:{}", self.universe() as u64, id & 1, id >> 1)
101 }
102 _ => self.0.to_string(),
103 }
104 }
105
106 pub fn from_steam2(steam2: &str) -> Result<Self, SteamIDError> {
107 lazy_static! {
108 static ref STEAM2_REGEX: Regex =
109 Regex::new(r"^STEAM_([0-4]):([0-1]):([0-9]{1,10})$").unwrap();
110 }
111
112 let groups = STEAM2_REGEX.captures(steam2).ok_or(SteamIDError::ParseError)?;
113
114 let mut universe: Universe = Universe::from_u64(
115 groups.get(1)
116 .ok_or(SteamIDError::ParseError)?
117 .as_str().parse().unwrap(),
118 ).ok_or(SteamIDError::ParseError)?;
119 let auth_server: u32 = groups.get(2)
120 .ok_or(SteamIDError::ParseError)?
121 .as_str().parse().unwrap();
122 let account_id: u32 = groups.get(3)
123 .ok_or(SteamIDError::ParseError)?
124 .as_str().parse().unwrap();
125 let account_id = account_id << 1 | auth_server;
126
127 if let Universe::Invalid = universe {
130 universe = Universe::Public;
131 }
132
133 Ok(Self::new(
134 account_id,
135 Instance::Desktop,
136 AccountType::Individual,
137 universe,
138 ))
139 }
140
141 pub fn steam3(&self) -> String {
142 let instance = self.instance();
143 let account_type = self.account_type();
144 let mut render_instance = false;
145
146 match account_type {
147 AccountType::AnonGameServer |
148 AccountType::Multiseat => render_instance = true,
149 AccountType::Individual => render_instance = instance != Instance::Desktop,
150 _ => (),
151 };
152
153 if render_instance {
154 format!(
155 "[{}:{}:{}:{}]",
156 account_type_to_char(account_type, instance),
157 self.universe() as u64,
158 self.account_id(),
159 instance as u64
160 )
161 } else {
162 format!(
163 "[{}:{}:{}]",
164 account_type_to_char(account_type, instance),
165 self.universe() as u64,
166 self.account_id()
167 )
168 }
169 }
170
171 pub fn from_steam3(steam3: &str) -> Result<Self, SteamIDError> {
172 lazy_static! {
173 static ref STEAM3_REGEX: Regex =
174 Regex::new(r"^\[([AGMPCgcLTIUai]):([0-4]):([0-9]{1,10})(:([0-9]+))?\]$").unwrap();
175 }
176
177 let groups = STEAM3_REGEX.captures(steam3).ok_or(SteamIDError::ParseError)?;
178
179 let type_char = groups.get(1)
180 .ok_or(SteamIDError::ParseError)?
181 .as_str().chars().next().unwrap();
182 let (account_type, flag) = char_to_account_type(type_char);
183 let universe = Universe::from_u64(
184 groups.get(2)
185 .ok_or(SteamIDError::ParseError)?
186 .as_str().parse().unwrap(),
187 ).ok_or(SteamIDError::ParseError)?;
188 let account_id = groups.get(3)
189 .ok_or(SteamIDError::ParseError)?
190 .as_str().parse().unwrap();
191
192 let mut instance: Option<Instance> = groups.get(5).map(|g| {
193 Instance::from_u64(g.as_str().parse().unwrap()).unwrap_or(Instance::Invalid)
194 });
195
196 if instance.is_none() && type_char == 'U' {
197 instance = Some(Instance::Desktop);
198 } else if type_char == 'T' || type_char == 'g' || instance.is_none() {
199 instance = Some(Instance::All);
200 }
201
202 if let Some(i) = flag {
203 instance = Some(i);
204 }
205
206 Ok(Self::new(
207 account_id,
208 instance.ok_or(SteamIDError::ParseError)?,
209 account_type,
210 universe,
211 ))
212 }
213}
214
215#[derive(Error, Debug)]
216pub enum SteamIDError {
217 #[error("Malformed SteamID")]
218 ParseError
219}
220
221impl From<u64> for SteamID {
222 fn from(s: u64) -> Self {
223 SteamID(s)
224 }
225}
226
227impl From<SteamID> for u64 {
228 fn from(s: SteamID) -> Self {
229 s.0
230 }
231}
232
233impl TryFrom<&str> for SteamID {
236 type Error = SteamIDError;
237
238 fn try_from(s: &str) -> Result<Self, Self::Error> {
239 match s.parse::<u64>() {
240 Ok(parsed) => Ok(parsed.into()),
241 Result::Err(_) => {
242 match Self::from_steam2(s) {
243 Ok(parsed) => Ok(parsed),
244 Result::Err(_) => Self::from_steam3(s),
245 }
246 }
247 }
248 }
249}
250
251pub struct SteamIDVisitor;
252
253impl<'de> Visitor<'de> for SteamIDVisitor {
254 type Value = SteamID;
255
256 fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
257 formatter.write_str("a SteamID")
258 }
259
260 fn visit_u64<E>(self, value: u64) -> Result<SteamID, E>
261 where
262 E: de::Error,
263 {
264 Ok(value.into())
265 }
266
267 fn visit_str<E>(self, value: &str) -> Result<SteamID, E>
268 where
269 E: de::Error,
270 {
271 SteamID::try_from(value).map_err(|_| E::custom(format!("Invalid SteamID: {}", value)))
272 }
273}
274
275impl<'de> Deserialize<'de> for SteamID {
276 fn deserialize<D>(deserializer: D) -> Result<SteamID, D::Error>
277 where
278 D: Deserializer<'de>,
279 {
280 deserializer.deserialize_any(SteamIDVisitor)
281 }
282}
283
284impl Debug for SteamID {
285 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
286 write!(
287 f,
288 "SteamID({}) {{ID: {}, Instance: {:?}, Type: {:?}, Universe: {:?}}}",
289 self.0,
290 self.account_id(),
291 self.instance(),
292 self.account_type(),
293 self.universe()
294 )
295 }
296}
297
298enum_from_primitive!{
299#[derive(Copy, Clone, PartialEq, Eq, Debug)]
300pub enum AccountType {
301 Invalid = 0,
302 Individual = 1,
303 Multiseat = 2,
304 GameServer = 3,
305 AnonGameServer = 4,
306 Pending = 5,
307 ContentServer = 6,
308 Clan = 7,
309 Chat = 8,
310 P2PSuperSeeder = 9,
311 AnonUser = 10,
312}
313}
314
315pub fn account_type_to_char(account_type: AccountType, instance: Instance) -> char {
316 match account_type {
317 AccountType::Invalid => 'I',
318 AccountType::Individual => 'U',
319 AccountType::Multiseat => 'M',
320 AccountType::GameServer => 'G',
321 AccountType::AnonGameServer => 'A',
322 AccountType::Pending => 'P',
323 AccountType::ContentServer => 'C',
324 AccountType::Clan => 'g',
325 AccountType::Chat => {
326 if let Instance::FlagClan = instance {
327 'c'
328 } else if let Instance::FlagLobby = instance {
329 'L'
330 } else {
331 'T'
332 }
333 }
334 AccountType::AnonUser => 'a',
335 AccountType::P2PSuperSeeder => 'i', }
337}
338
339pub fn char_to_account_type(c: char) -> (AccountType, Option<Instance>) {
342 match c {
343 'U' => (AccountType::Individual, None),
344 'M' => (AccountType::Multiseat, None),
345 'G' => (AccountType::GameServer, None),
346 'A' => (AccountType::AnonGameServer, None),
347 'P' => (AccountType::Pending, None),
348 'C' => (AccountType::ContentServer, None),
349 'g' => (AccountType::Clan, None),
350
351 'T' => (AccountType::Chat, None),
352 'c' => (AccountType::Chat, Some(Instance::FlagClan)),
353 'L' => (AccountType::Chat, Some(Instance::FlagLobby)),
354
355 'a' => (AccountType::AnonUser, None),
356
357 'I' | 'i' | _ => (AccountType::Invalid, None),
358 }
359}
360
361enum_from_primitive! {
362#[derive(Copy, Clone, PartialEq, Eq, Debug)]
363pub enum Universe {
364 Invalid = 0,
365 Public = 1,
366 Beta = 2,
367 Internal = 3,
368 Dev = 4,
369}
370}
371
372enum_from_primitive! {
373#[derive(Copy, Clone, PartialEq, Eq, Debug)]
374pub enum Instance {
375 All = 0,
376 Desktop = 1,
377 Console = 2,
378 Web = 4,
379 Invalid = 666,
381 FlagClan = 0x100000 >> 1,
383 FlagLobby = 0x100000 >> 2,
384 FlagMMSLobby = 0x100000 >> 3,
385}
386}