steamid_ng/
lib.rs

1//! # SteamID
2//! The steamid-ng crate provides an easy-to-use SteamID type with functions to parse and render
3//! steam2 and steam3 IDs. It also supports serializing and deserializing via
4//! [serde](https://serde.rs).
5//!
6//! ## Examples
7//!
8//! ```
9//! # use steamid_ng::{SteamID, Instance, AccountType, Universe};
10//! let x = SteamID::from(76561197960287930);
11//! let y = SteamID::from_steam3("[U:1:22202]").unwrap();
12//! let z = SteamID::from_steam2("STEAM_1:0:11101").unwrap();
13//! assert_eq!(x, y);
14//! assert_eq!(y, z);
15//!
16//! assert_eq!(u64::from(z), 76561197960287930);
17//! assert_eq!(y.steam2(), "STEAM_1:0:11101");
18//! assert_eq!(x.steam3(), "[U:1:22202]");
19//!
20//! assert_eq!(x.account_id(), 22202);
21//! assert_eq!(x.instance(), Instance::Desktop);
22//! assert_eq!(x.account_type(), AccountType::Individual);
23//! assert_eq!(x.universe(), Universe::Public);
24//! // the SteamID type also has `set_{account_id, instance, account_type, universe}` methods,
25//! // which work as you would expect.
26//! ```
27//!
28//! Keep in mind that the SteamID type does no validation.
29
30
31use 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        // only ever 32 bits
48        (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        // Apparently, games before orange box used to display as 0 incorrectly
128        // This is only an issue with steam2 ids
129        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
233// TODO: convert this to TryFrom once it's out of nightly
234// There will probably be a blanket impl that provides FromStr automatically
235impl 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', // Invalid (?)
336    }
337}
338
339/// In certain cases, this function will return an Instance as the second item in the tuple. You
340/// should set the instance of the underlying SteamID to this value.
341pub 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    // Made up magic constant
380    Invalid = 666,
381    // *Apparently*, All will by the only type used if any of these is set
382    FlagClan = 0x100000 >> 1,
383    FlagLobby = 0x100000 >> 2,
384    FlagMMSLobby = 0x100000 >> 3,
385}
386}