1#[cfg(feature = "serde")]
31use serde::{
32 Deserialize, Deserializer, Serialize,
33 de::{self, Visitor},
34};
35use std::{
36 error::Error,
37 fmt::{self, Debug, Display, Formatter},
38 str::FromStr,
39};
40
41#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)]
42pub struct SteamIDParseError;
43
44impl Error for SteamIDParseError {}
45
46impl Display for SteamIDParseError {
47 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
48 write!(f, "Malformed SteamID")
49 }
50}
51
52fn digit_from_ascii(byte: u8) -> Option<u8> {
53 if byte.is_ascii_digit() {
54 Some(byte - b'0')
55 } else {
56 None
57 }
58}
59
60#[cfg_attr(feature = "serde", derive(Serialize))]
61#[derive(Clone, Copy, PartialEq, Eq, Hash)]
62pub struct SteamID(u64);
63
64impl SteamID {
65 pub fn new(
66 account_id: u32,
67 instance: Instance,
68 account_type: AccountType,
69 universe: Universe,
70 ) -> Self {
71 Self(
72 (account_id as u64)
73 | ((instance.0 as u64) << 32)
74 | ((account_type as u64) << 52)
75 | ((universe as u64) << 56),
76 )
77 }
78
79 pub fn account_id(&self) -> u32 {
80 (self.0 & 0xFFFFFFFF) as u32
81 }
82
83 pub fn set_account_id(&mut self, account_id: u32) {
84 self.0 &= 0xFFFFFFFF00000000;
85 self.0 |= account_id as u64;
86 }
87
88 pub fn instance(&self) -> Instance {
89 Instance(((self.0 >> 32) & 0xFFFFF) as u32)
90 }
91
92 pub fn set_instance(&mut self, instance: Instance) {
93 self.0 &= 0xFFF00000FFFFFFFF;
94 self.0 |= (instance.0 as u64) << 32;
95 }
96
97 pub fn set_instance_type(&mut self, instance_type: InstanceType) {
98 let mut instance = self.instance();
99 instance.set_instance_type(instance_type);
100 self.set_instance(instance);
101 }
102
103 pub fn set_instance_flags(&mut self, instance_flags: InstanceFlags) {
104 let mut instance = self.instance();
105 instance.set_flags(instance_flags);
106 self.set_instance(instance);
107 }
108
109 pub fn account_type(&self) -> AccountType {
110 AccountType::try_from(((self.0 >> 52) & 0xF) as u8).expect("Account type should be valid")
111 }
112
113 pub fn set_account_type(&mut self, account_type: AccountType) {
114 self.0 &= 0xFF0FFFFFFFFFFFFF;
115 self.0 |= (account_type as u64) << 52;
116 }
117
118 pub fn universe(&self) -> Universe {
119 Universe::try_from(((self.0 >> 56) & 0xFF) as u8).expect("Universe should be valid")
120 }
121
122 pub fn set_universe(&mut self, universe: Universe) {
123 self.0 &= 0x00FFFFFFFFFFFFFF;
124 self.0 |= (universe as u64) << 56;
125 }
126
127 pub fn steam64(&self) -> u64 {
128 self.0
129 }
130
131 pub fn from_steam64(value: u64) -> Result<Self, SteamIDParseError> {
132 Self::try_from(value)
133 }
134
135 pub fn steam2(&self) -> String {
136 match self.account_type() {
137 AccountType::Individual | AccountType::Invalid => {
138 let id = self.account_id();
139 format!("STEAM_{}:{}:{}", self.universe() as u64, id & 1, id >> 1)
140 }
141 _ => self.0.to_string(),
142 }
143 }
144
145 pub fn from_steam2(steam2: &str) -> Result<Self, SteamIDParseError> {
148 let chunk = steam2.strip_prefix("STEAM_").ok_or(SteamIDParseError)?;
149 let mut bytes = chunk.bytes();
150
151 let mut universe: Universe = bytes
152 .next()
153 .and_then(digit_from_ascii)
154 .ok_or(SteamIDParseError)
155 .and_then(Universe::try_from)?;
156 if let Universe::Invalid = universe {
159 universe = Universe::Public;
160 }
161
162 if bytes.next() != Some(b':') {
163 return Err(SteamIDParseError);
164 }
165
166 let auth_server: u32 = match bytes.next().ok_or(SteamIDParseError)? {
167 b'0' => Ok(0),
168 b'1' => Ok(1),
169 _ => Err(SteamIDParseError),
170 }?;
171
172 if bytes.next() != Some(b':') {
173 return Err(SteamIDParseError);
174 }
175
176 if bytes.len() > 10 {
177 return Err(SteamIDParseError);
178 }
179
180 let mut account_id = bytes
181 .next()
182 .and_then(digit_from_ascii)
183 .ok_or(SteamIDParseError)? as u32;
184 for b in bytes {
185 let digit = digit_from_ascii(b).ok_or(SteamIDParseError)? as u32;
186 account_id = account_id
187 .checked_mul(10)
188 .and_then(|id| id.checked_add(digit))
189 .ok_or(SteamIDParseError)?;
190 }
191 let account_id = account_id << 1 | auth_server;
192
193 Ok(Self::new(
194 account_id,
195 Instance::new(InstanceType::Desktop, InstanceFlags::None),
196 AccountType::Individual,
197 universe,
198 ))
199 }
200
201 pub fn steam3(&self) -> String {
202 let account_type = self.account_type();
203 let instance = self.instance();
204 let instance_type = instance.instance_type();
205 let instance_flags = instance.flags();
206 let mut render_instance = false;
207
208 match account_type {
209 AccountType::AnonGameServer | AccountType::Multiseat => render_instance = true,
210 AccountType::Individual => render_instance = instance_type != Some(InstanceType::Desktop),
211 _ => (),
212 };
213
214 if render_instance {
215 format!(
216 "[{}:{}:{}:{}]",
217 account_type_to_char(account_type, instance_flags),
218 self.universe() as u64,
219 self.account_id(),
220 instance.0
221 )
222 } else {
223 format!(
224 "[{}:{}:{}]",
225 account_type_to_char(account_type, instance_flags),
226 self.universe() as u64,
227 self.account_id()
228 )
229 }
230 }
231
232 pub fn from_steam3(steam3: &str) -> Result<Self, SteamIDParseError> {
235 let mut bytes = steam3.bytes().peekable();
236
237 if bytes.next() != Some(b'[') {
238 return Err(SteamIDParseError);
239 }
240
241 let (account_type, instance_flags) = bytes
242 .next()
243 .and_then(|b| char_to_account_type(b.into()))
244 .ok_or(SteamIDParseError)?;
245
246 if bytes.next() != Some(b':') {
247 return Err(SteamIDParseError);
248 }
249
250 let universe = bytes
251 .next()
252 .and_then(digit_from_ascii)
253 .ok_or(SteamIDParseError)
254 .and_then(Universe::try_from)?;
255
256 if bytes.next() != Some(b':') {
257 return Err(SteamIDParseError);
258 }
259
260 let mut account_id = bytes
261 .next()
262 .and_then(digit_from_ascii)
263 .ok_or(SteamIDParseError)? as u32;
264 while let Some(digit) = bytes.peek().copied().and_then(digit_from_ascii) {
265 bytes.next().expect("Byte was peeked");
266 account_id = account_id
267 .checked_mul(10)
268 .and_then(|id| id.checked_add(digit as u32))
269 .ok_or(SteamIDParseError)?;
270 }
271
272 let instance_type = {
274 let maybe_instance_type = if bytes.peek().copied() == Some(b':') {
275 bytes.next().expect("Byte was peeked");
276
277 let mut acc = bytes
278 .next()
279 .and_then(digit_from_ascii)
280 .ok_or(SteamIDParseError)? as u32;
281 while let Some(digit) = bytes.peek().copied().and_then(digit_from_ascii) {
282 bytes.next().expect("Byte was peeked");
283 acc = acc
284 .checked_mul(10)
285 .and_then(|id| id.checked_add(digit as u32))
286 .ok_or(SteamIDParseError)?;
287 }
288
289 Some(InstanceType::try_from(acc)?)
290 } else {
291 None
292 };
293
294 match (maybe_instance_type, account_type) {
295 (None, AccountType::Individual) => InstanceType::Desktop,
296 (None, _) => InstanceType::All,
297 (Some(_), AccountType::Clan | AccountType::Chat) => InstanceType::All,
298 (Some(instance_type), _) => instance_type,
299 }
300 };
301
302 if bytes.next() != Some(b']') || bytes.next().is_some() {
303 return Err(SteamIDParseError);
304 }
305
306 Ok(Self::new(
307 account_id,
308 Instance::new(instance_type, instance_flags),
309 account_type,
310 universe,
311 ))
312 }
313}
314
315impl TryFrom<u64> for SteamID {
316 type Error = SteamIDParseError;
317
318 fn try_from(value: u64) -> Result<Self, Self::Error> {
319 AccountType::try_from((value >> 52 & 0xF) as u8)?;
320 Universe::try_from((value >> 56 & 0xFF) as u8)?;
321
322 Ok(SteamID(value))
323 }
324}
325
326impl From<SteamID> for u64 {
327 fn from(s: SteamID) -> Self {
328 s.0
329 }
330}
331
332impl FromStr for SteamID {
333 type Err = SteamIDParseError;
334 fn from_str(s: &str) -> Result<Self, Self::Err> {
338 if let Ok(u) = s.parse::<u64>() {
339 SteamID::try_from(u)
340 } else if let Ok(s) = Self::from_steam2(s) {
341 Ok(s)
342 } else if let Ok(s) = Self::from_steam3(s) {
343 Ok(s)
344 } else {
345 Err(SteamIDParseError)
346 }
347 }
348}
349
350#[cfg(feature = "serde")]
351struct SteamIDVisitor;
352#[cfg(feature = "serde")]
353impl<'de> Visitor<'de> for SteamIDVisitor {
354 type Value = SteamID;
355
356 fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
357 formatter.write_str("a SteamID")
358 }
359
360 fn visit_u64<E>(self, value: u64) -> Result<SteamID, E>
361 where
362 E: de::Error,
363 {
364 SteamID::try_from(value).map_err(|_| E::custom(format!("invalid SteamID: {}", value)))
365 }
366
367 fn visit_str<E>(self, value: &str) -> Result<SteamID, E>
368 where
369 E: de::Error,
370 {
371 SteamID::from_str(value).map_err(|_| E::custom(format!("invalid SteamID: {}", value)))
372 }
373}
374
375#[cfg(feature = "serde")]
376impl<'de> Deserialize<'de> for SteamID {
377 fn deserialize<D>(deserializer: D) -> Result<SteamID, D::Error>
378 where
379 D: Deserializer<'de>,
380 {
381 deserializer.deserialize_any(SteamIDVisitor)
382 }
383}
384
385impl Debug for SteamID {
386 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
387 write!(
388 f,
389 "SteamID({}) {{ID: {}, Instance: {:?}, Type: {:?}, Universe: {:?}}}",
390 self.0,
391 self.account_id(),
392 self.instance(),
393 self.account_type(),
394 self.universe()
395 )
396 }
397}
398
399#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
400pub enum AccountType {
401 Invalid = 0,
402 Individual = 1,
403 Multiseat = 2,
404 GameServer = 3,
405 AnonGameServer = 4,
406 Pending = 5,
407 ContentServer = 6,
408 Clan = 7,
409 Chat = 8,
410 ConsoleUser = 9,
411 AnonUser = 10,
412}
413
414impl TryFrom<u8> for AccountType {
415 type Error = SteamIDParseError;
416 fn try_from(value: u8) -> Result<Self, Self::Error> {
417 match value {
418 0 => Ok(AccountType::Invalid),
419 1 => Ok(AccountType::Individual),
420 2 => Ok(AccountType::Multiseat),
421 3 => Ok(AccountType::GameServer),
422 4 => Ok(AccountType::AnonGameServer),
423 5 => Ok(AccountType::Pending),
424 6 => Ok(AccountType::ContentServer),
425 7 => Ok(AccountType::Clan),
426 8 => Ok(AccountType::Chat),
427 9 => Ok(AccountType::ConsoleUser),
428 10 => Ok(AccountType::AnonUser),
429 _ => Err(SteamIDParseError),
430 }
431 }
432}
433
434pub fn account_type_to_char(account_type: AccountType, flags: Option<InstanceFlags>) -> char {
435 match account_type {
436 AccountType::Invalid => 'I',
437 AccountType::Individual => 'U',
438 AccountType::Multiseat => 'M',
439 AccountType::GameServer => 'G',
440 AccountType::AnonGameServer => 'A',
441 AccountType::Pending => 'P',
442 AccountType::ContentServer => 'C',
443 AccountType::Clan => 'g',
444 AccountType::Chat => match flags {
445 Some(InstanceFlags::Clan) => 'c',
446 Some(InstanceFlags::Lobby) => 'L',
447 _ => 'T',
448 },
449 AccountType::ConsoleUser => 'U',
450 AccountType::AnonUser => 'a',
451 }
452}
453
454pub fn char_to_account_type(c: char) -> Option<(AccountType, InstanceFlags)> {
455 match c {
456 'U' => Some((AccountType::Individual, InstanceFlags::None)),
457 'M' => Some((AccountType::Multiseat, InstanceFlags::None)),
458 'G' => Some((AccountType::GameServer, InstanceFlags::None)),
459 'A' => Some((AccountType::AnonGameServer, InstanceFlags::None)),
460 'P' => Some((AccountType::Pending, InstanceFlags::None)),
461 'C' => Some((AccountType::ContentServer, InstanceFlags::None)),
462 'g' => Some((AccountType::Clan, InstanceFlags::None)),
463 'T' => Some((AccountType::Chat, InstanceFlags::None)),
464 'c' => Some((AccountType::Chat, InstanceFlags::Clan)),
465 'L' => Some((AccountType::Chat, InstanceFlags::Lobby)),
466 'a' => Some((AccountType::AnonUser, InstanceFlags::None)),
467 'I' | 'i' => Some((AccountType::Invalid, InstanceFlags::None)),
468 _ => None,
469 }
470}
471
472#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
473pub enum Universe {
474 Invalid = 0,
475 Public = 1,
476 Beta = 2,
477 Internal = 3,
478 Dev = 4,
479 RC = 5,
480}
481
482impl TryFrom<u8> for Universe {
483 type Error = SteamIDParseError;
484 fn try_from(value: u8) -> Result<Self, Self::Error> {
485 match value {
486 0 => Ok(Universe::Invalid),
487 1 => Ok(Universe::Public),
488 2 => Ok(Universe::Beta),
489 3 => Ok(Universe::Internal),
490 4 => Ok(Universe::Dev),
491 5 => Ok(Universe::RC),
492 _ => Err(SteamIDParseError),
493 }
494 }
495}
496
497#[derive(Copy, Clone, PartialEq, Eq, Hash)]
498pub struct Instance(pub u32);
499
500impl Instance {
501 pub fn new(instance_type: InstanceType, flags: InstanceFlags) -> Self {
502 Instance(instance_type as u32 | (flags as u32) << 12)
503 }
504
505 pub fn instance_type(&self) -> Option<InstanceType> {
506 match self.0 & 0xFFF {
507 0 => Some(InstanceType::All),
508 1 => Some(InstanceType::Desktop),
509 2 => Some(InstanceType::Console),
510 4 => Some(InstanceType::Web),
511 _ => None,
512 }
513 }
514
515 pub fn set_instance_type(&mut self, instance_type: InstanceType) {
516 self.0 &= 0xFF000;
517 self.0 |= instance_type as u32;
518 }
519
520 pub fn flags(&self) -> Option<InstanceFlags> {
521 match self.0 >> 12 {
522 0 => Some(InstanceFlags::None),
523 0b1000_0000 => Some(InstanceFlags::Clan),
524 0b0100_0000 => Some(InstanceFlags::Lobby),
525 0b0010_0000 => Some(InstanceFlags::MMSLobby),
526 _ => None,
527 }
528 }
529
530 pub fn set_flags(&mut self, flags: InstanceFlags) {
531 self.0 &= 0x00FFF;
532 self.0 |= (flags as u32) << 12;
533 }
534}
535
536impl Debug for Instance {
537 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
538 if let Some(instance_type) = self.instance_type() && let Some(flags) = self.flags() {
539 write!(
540 f,
541 "{{Type: {:?}, Flags: {:?}}}",
542 instance_type,
543 flags
544 )
545 } else {
546 write!(f, "{}", self.0)
547 }
548 }
549}
550
551#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
552pub enum InstanceType {
553 All = 0,
554 Desktop = 1,
555 Console = 2,
556 Web = 4,
557}
558
559impl TryFrom<u32> for InstanceType {
560 type Error = SteamIDParseError;
561 fn try_from(value: u32) -> Result<Self, Self::Error> {
562 match value {
563 0 => Ok(InstanceType::All),
564 1 => Ok(InstanceType::Desktop),
565 2 => Ok(InstanceType::Console),
566 4 => Ok(InstanceType::Web),
567 _ => Err(SteamIDParseError),
568 }
569 }
570}
571
572#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Default)]
573pub enum InstanceFlags {
574 #[default]
575 None = 0,
576 Clan = 0b1000_0000,
577 Lobby = 0b0100_0000,
578 MMSLobby = 0b0010_0000,
579}
580
581impl TryFrom<u8> for InstanceFlags {
582 type Error = SteamIDParseError;
583 fn try_from(value: u8) -> Result<Self, Self::Error> {
584 match value {
585 0 => Ok(InstanceFlags::None),
586 0b1000_0000 => Ok(InstanceFlags::Clan),
587 0b0100_0000 => Ok(InstanceFlags::Lobby),
588 0b0010_0000 => Ok(InstanceFlags::MMSLobby),
589 _ => Err(SteamIDParseError),
590 }
591 }
592}