1use std::{fmt, str::FromStr, sync::LazyLock};
4
5use regex::Regex;
6
7use crate::{
8 enums::{chat_instance_flags, masks, AccountType, Instance, Universe},
9 error::SteamIdError,
10};
11
12static STEAM2_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^STEAM_([0-5]):([0-1]):([0-9]+)$").unwrap());
14static STEAM3_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\[([a-zA-Z]):([0-5]):([0-9]+)(:[0-9]+)?\]$").unwrap());
15static STEAMID64_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\d+$").unwrap());
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub struct SteamID {
20 pub universe: Universe,
22 pub account_type: AccountType,
24 pub instance: u32,
26 pub account_id: u32,
28}
29
30impl Default for SteamID {
31 fn default() -> Self {
32 Self::new()
33 }
34}
35
36impl SteamID {
37 pub fn new() -> Self {
39 SteamID {
40 universe: Universe::Invalid,
41 account_type: AccountType::Invalid,
42 instance: Instance::All as u32,
43 account_id: 0,
44 }
45 }
46
47 pub fn from_individual_account_id(account_id: u32) -> Self {
52 SteamID {
53 universe: Universe::Public,
54 account_type: AccountType::Individual,
55 instance: Instance::Desktop as u32,
56 account_id,
57 }
58 }
59
60 pub fn from_steam_id64(id: u64) -> Self {
62 let account_id = (id & masks::ACCOUNT_ID_MASK) as u32;
63 let instance = ((id >> 32) & masks::ACCOUNT_INSTANCE_MASK as u64) as u32;
64 let account_type = AccountType::from_u8(((id >> 52) & 0xF) as u8);
65 let universe = Universe::from_u8((id >> 56) as u8);
66
67 SteamID { universe, account_type, instance, account_id }
68 }
69
70 pub fn is_valid(&self) -> bool {
76 if self.account_type == AccountType::Invalid || (self.account_type as u8) > 10 {
78 return false;
79 }
80
81 if self.universe == Universe::Invalid || (self.universe as u8) > 4 {
83 return false;
84 }
85
86 if self.account_type == AccountType::Individual && (self.account_id == 0 || self.instance > Instance::Web as u32) {
88 return false;
89 }
90
91 if self.account_type == AccountType::Clan && (self.account_id == 0 || self.instance != Instance::All as u32) {
93 return false;
94 }
95
96 if self.account_type == AccountType::GameServer && self.account_id == 0 {
98 return false;
99 }
100
101 true
102 }
103
104 pub fn is_valid_individual(&self) -> bool {
110 self.universe == Universe::Public && self.account_type == AccountType::Individual && self.instance == Instance::Desktop as u32 && self.is_valid()
111 }
112
113 pub fn is_group_chat(&self) -> bool {
115 self.account_type == AccountType::Chat && (self.instance & chat_instance_flags::CLAN) != 0
116 }
117
118 pub fn is_lobby(&self) -> bool {
120 self.account_type == AccountType::Chat && ((self.instance & chat_instance_flags::LOBBY) != 0 || (self.instance & chat_instance_flags::MMS_LOBBY) != 0)
121 }
122
123 pub fn steam2(&self, newer_format: bool) -> Result<String, SteamIdError> {
132 if self.account_type != AccountType::Individual {
133 return Err(SteamIdError::NotIndividual);
134 }
135
136 let universe = if !newer_format && self.universe == Universe::Public { 0 } else { self.universe as u8 };
137
138 Ok(format!("STEAM_{}:{}:{}", universe, self.account_id & 1, self.account_id / 2))
139 }
140
141 pub fn steam3(&self) -> String {
143 let mut type_char = self.account_type.to_char();
144
145 if (self.instance & chat_instance_flags::CLAN) != 0 {
147 type_char = 'c';
148 } else if (self.instance & chat_instance_flags::LOBBY) != 0 {
149 type_char = 'L';
150 }
151
152 let should_render_instance = self.account_type == AccountType::AnonGameServer || self.account_type == AccountType::Multiseat || (self.account_type == AccountType::Individual && self.instance != Instance::Desktop as u32);
153
154 if should_render_instance {
155 format!("[{}:{}:{}:{}]", type_char, self.universe as u8, self.account_id, self.instance)
156 } else {
157 format!("[{}:{}:{}]", type_char, self.universe as u8, self.account_id)
158 }
159 }
160
161 pub fn steam_id64(&self) -> u64 {
163 let universe = (self.universe as u64) << 56;
164 let account_type = (self.account_type as u64) << 52;
165 let instance = (self.instance as u64) << 32;
166 let account_id = self.account_id as u64;
167
168 universe | account_type | instance | account_id
169 }
170
171 fn parse_steam2(input: &str) -> Option<Self> {
173 let caps = STEAM2_REGEX.captures(input)?;
174
175 let universe_num: u8 = caps.get(1)?.as_str().parse().ok()?;
176 let mod_num: u32 = caps.get(2)?.as_str().parse().ok()?;
177 let account_id_half: u32 = caps.get(3)?.as_str().parse().ok()?;
178
179 let universe = if universe_num == 0 { Universe::Public } else { Universe::from_u8(universe_num) };
181
182 Some(SteamID {
183 universe,
184 account_type: AccountType::Individual,
185 instance: Instance::Desktop as u32,
186 account_id: (account_id_half * 2) + mod_num,
187 })
188 }
189
190 fn parse_steam3(input: &str) -> Option<Self> {
191 let caps = STEAM3_REGEX.captures(input)?;
192
193 let type_char = caps.get(1)?.as_str().chars().next()?;
194 let universe_num: u8 = caps.get(2)?.as_str().parse().ok()?;
195 let account_id: u32 = caps.get(3)?.as_str().parse().ok()?;
196
197 let universe = Universe::from_u8(universe_num);
198
199 let mut instance: u32 = Instance::All as u32;
200 if let Some(instance_match) = caps.get(4) {
201 let instance_str = &instance_match.as_str()[1..];
203 instance = instance_str.parse().ok()?;
204 }
205
206 let account_type = match type_char {
207 'U' => {
208 if caps.get(4).is_none() {
210 instance = Instance::Desktop as u32;
211 }
212 AccountType::Individual
213 }
214 'c' => {
215 instance |= chat_instance_flags::CLAN;
216 AccountType::Chat
217 }
218 'L' => {
219 instance |= chat_instance_flags::LOBBY;
220 AccountType::Chat
221 }
222 _ => AccountType::from_char(type_char),
223 };
224
225 Some(SteamID { universe, account_type, instance, account_id })
226 }
227
228 fn parse_steam_id64(input: &str) -> Option<Self> {
229 if !STEAMID64_REGEX.is_match(input) {
230 return None;
231 }
232
233 let id: u64 = input.parse().ok()?;
234 Some(Self::from_steam_id64(id))
235 }
236}
237
238impl FromStr for SteamID {
239 type Err = SteamIdError;
240
241 fn from_str(input: &str) -> Result<Self, Self::Err> {
242 if let Some(sid) = Self::parse_steam2(input) {
244 return Ok(sid);
245 }
246
247 if let Some(sid) = Self::parse_steam3(input) {
249 return Ok(sid);
250 }
251
252 if let Some(sid) = Self::parse_steam_id64(input) {
254 return Ok(sid);
255 }
256
257 Err(SteamIdError::InvalidFormat(input.to_string()))
258 }
259}
260
261impl TryFrom<&str> for SteamID {
262 type Error = SteamIdError;
263
264 fn try_from(value: &str) -> Result<Self, Self::Error> {
265 value.parse()
266 }
267}
268
269impl TryFrom<String> for SteamID {
270 type Error = SteamIdError;
271
272 fn try_from(value: String) -> Result<Self, Self::Error> {
273 value.parse()
274 }
275}
276
277impl From<u64> for SteamID {
278 fn from(value: u64) -> Self {
279 Self::from_steam_id64(value)
280 }
281}
282
283impl fmt::Display for SteamID {
284 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
285 write!(f, "{}", self.steam_id64())
286 }
287}
288
289impl serde::Serialize for SteamID {
290 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
291 where
292 S: serde::Serializer,
293 {
294 serializer.serialize_u64(self.steam_id64())
295 }
296}
297
298impl<'de> serde::Deserialize<'de> for SteamID {
299 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
300 where
301 D: serde::Deserializer<'de>,
302 {
303 struct SteamIDVisitor;
304
305 impl<'de> serde::de::Visitor<'de> for SteamIDVisitor {
306 type Value = SteamID;
307
308 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
309 formatter.write_str("a SteamID64 as a number or string")
310 }
311
312 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
313 where
314 E: serde::de::Error,
315 {
316 Ok(SteamID::from(value))
317 }
318
319 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
320 where
321 E: serde::de::Error,
322 {
323 value.parse().map_err(serde::de::Error::custom)
324 }
325 }
326
327 deserializer.deserialize_any(SteamIDVisitor)
328 }
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334
335 #[test]
336 fn test_parameterless_construction() {
337 let sid = SteamID::new();
338 assert_eq!(sid.universe, Universe::Invalid);
339 assert_eq!(sid.account_type, AccountType::Invalid);
340 assert_eq!(sid.instance, Instance::All as u32);
341 assert_eq!(sid.account_id, 0);
342 }
343
344 #[test]
345 fn test_from_individual_account_id() {
346 let sid = SteamID::from_individual_account_id(46143802);
347 assert_eq!(sid.universe, Universe::Public);
348 assert_eq!(sid.account_type, AccountType::Individual);
349 assert_eq!(sid.instance, Instance::Desktop as u32);
350 assert_eq!(sid.account_id, 46143802);
351 assert!(sid.is_valid());
352 assert!(sid.is_valid_individual());
353 }
354
355 #[test]
356 fn test_steam2id_construction_universe_0() {
357 let sid: SteamID = "STEAM_0:0:23071901".parse().unwrap();
358 assert_eq!(sid.universe, Universe::Public);
359 assert_eq!(sid.account_type, AccountType::Individual);
360 assert_eq!(sid.instance, Instance::Desktop as u32);
361 assert_eq!(sid.account_id, 46143802);
362 }
363
364 #[test]
365 fn test_steam2id_construction_universe_1() {
366 let sid: SteamID = "STEAM_1:1:23071901".parse().unwrap();
367 assert_eq!(sid.universe, Universe::Public);
368 assert_eq!(sid.account_type, AccountType::Individual);
369 assert_eq!(sid.instance, Instance::Desktop as u32);
370 assert_eq!(sid.account_id, 46143803);
371 }
372
373 #[test]
374 fn test_steam3id_construction_individual() {
375 let sid: SteamID = "[U:1:46143802]".parse().unwrap();
376 assert_eq!(sid.universe, Universe::Public);
377 assert_eq!(sid.account_type, AccountType::Individual);
378 assert_eq!(sid.instance, Instance::Desktop as u32);
379 assert_eq!(sid.account_id, 46143802);
380 }
381
382 #[test]
383 fn test_steam3id_construction_gameserver() {
384 let sid: SteamID = "[G:1:31]".parse().unwrap();
385 assert_eq!(sid.universe, Universe::Public);
386 assert_eq!(sid.account_type, AccountType::GameServer);
387 assert_eq!(sid.instance, Instance::All as u32);
388 assert_eq!(sid.account_id, 31);
389 assert!(sid.is_valid());
390 assert!(!sid.is_valid_individual());
391 }
392
393 #[test]
394 fn test_steam3id_construction_anon_gameserver() {
395 let sid: SteamID = "[A:1:46124:11245]".parse().unwrap();
396 assert_eq!(sid.universe, Universe::Public);
397 assert_eq!(sid.account_type, AccountType::AnonGameServer);
398 assert_eq!(sid.instance, 11245);
399 assert_eq!(sid.account_id, 46124);
400 }
401
402 #[test]
403 fn test_steam3id_construction_lobby() {
404 let sid: SteamID = "[L:1:12345]".parse().unwrap();
405 assert_eq!(sid.universe, Universe::Public);
406 assert_eq!(sid.account_type, AccountType::Chat);
407 assert_eq!(sid.instance, chat_instance_flags::LOBBY);
408 assert_eq!(sid.account_id, 12345);
409 }
410
411 #[test]
412 fn test_steam3id_construction_lobby_with_instanceid() {
413 let sid: SteamID = "[L:1:12345:55]".parse().unwrap();
414 assert_eq!(sid.universe, Universe::Public);
415 assert_eq!(sid.account_type, AccountType::Chat);
416 assert_eq!(sid.instance, chat_instance_flags::LOBBY | 55);
417 assert_eq!(sid.account_id, 12345);
418 }
419
420 #[test]
421 fn test_steamid64_construction_individual() {
422 let sid: SteamID = "76561198006409530".parse().unwrap();
423 assert_eq!(sid.universe, Universe::Public);
424 assert_eq!(sid.account_type, AccountType::Individual);
425 assert_eq!(sid.instance, Instance::Desktop as u32);
426 assert_eq!(sid.account_id, 46143802);
427 }
428
429 #[test]
430 fn test_steamid64_construction_clan() {
431 let sid: SteamID = "103582791434202956".parse().unwrap();
432 assert_eq!(sid.universe, Universe::Public);
433 assert_eq!(sid.account_type, AccountType::Clan);
434 assert_eq!(sid.instance, Instance::All as u32);
435 assert_eq!(sid.account_id, 4681548);
436 }
437
438 #[test]
439 fn test_steamid64_from_u64() {
440 let sid = SteamID::from(76561198006409530u64);
441 assert_eq!(sid.universe, Universe::Public);
442 assert_eq!(sid.account_type, AccountType::Individual);
443 assert_eq!(sid.instance, Instance::Desktop as u32);
444 assert_eq!(sid.account_id, 46143802);
445 }
446
447 #[test]
448 fn test_invalid_construction() {
449 let result: Result<SteamID, _> = "invalid input".parse();
450 assert!(result.is_err());
451 }
452
453 #[test]
454 fn test_steam2id_rendering_universe_0() {
455 let mut sid = SteamID::new();
456 sid.universe = Universe::Public;
457 sid.account_type = AccountType::Individual;
458 sid.instance = Instance::Desktop as u32;
459 sid.account_id = 46143802;
460 assert_eq!(sid.steam2(false).unwrap(), "STEAM_0:0:23071901");
461 }
462
463 #[test]
464 fn test_steam2id_rendering_universe_1() {
465 let mut sid = SteamID::new();
466 sid.universe = Universe::Public;
467 sid.account_type = AccountType::Individual;
468 sid.instance = Instance::Desktop as u32;
469 sid.account_id = 46143802;
470 assert_eq!(sid.steam2(true).unwrap(), "STEAM_1:0:23071901");
471 }
472
473 #[test]
474 fn test_steam2id_rendering_non_individual() {
475 let mut sid = SteamID::new();
476 sid.universe = Universe::Public;
477 sid.account_type = AccountType::Clan;
478 sid.instance = Instance::Desktop as u32;
479 sid.account_id = 4681548;
480 assert!(sid.steam2(false).is_err());
481 }
482
483 #[test]
484 fn test_steam3id_rendering_individual() {
485 let mut sid = SteamID::new();
486 sid.universe = Universe::Public;
487 sid.account_type = AccountType::Individual;
488 sid.instance = Instance::Desktop as u32;
489 sid.account_id = 46143802;
490 assert_eq!(sid.steam3(), "[U:1:46143802]");
491 }
492
493 #[test]
494 fn test_steam3id_rendering_anon_gameserver() {
495 let mut sid = SteamID::new();
496 sid.universe = Universe::Public;
497 sid.account_type = AccountType::AnonGameServer;
498 sid.instance = 41511;
499 sid.account_id = 43253156;
500 assert_eq!(sid.steam3(), "[A:1:43253156:41511]");
501 }
502
503 #[test]
504 fn test_steam3id_rendering_lobby() {
505 let mut sid = SteamID::new();
506 sid.universe = Universe::Public;
507 sid.account_type = AccountType::Chat;
508 sid.instance = chat_instance_flags::LOBBY;
509 sid.account_id = 451932;
510 assert_eq!(sid.steam3(), "[L:1:451932]");
511 }
512
513 #[test]
514 fn test_steamid64_rendering_individual() {
515 let mut sid = SteamID::new();
516 sid.universe = Universe::Public;
517 sid.account_type = AccountType::Individual;
518 sid.instance = Instance::Desktop as u32;
519 sid.account_id = 46143802;
520 assert_eq!(sid.steam_id64(), 76561198006409530);
521 assert_eq!(sid.to_string(), "76561198006409530");
522 }
523
524 #[test]
525 fn test_steamid64_rendering_anon_gameserver() {
526 let mut sid = SteamID::new();
527 sid.universe = Universe::Public;
528 sid.account_type = AccountType::AnonGameServer;
529 sid.instance = 188991;
530 sid.account_id = 42135013;
531 assert_eq!(sid.steam_id64(), 90883702753783269);
532 }
533
534 #[test]
535 fn test_invalid_new_id() {
536 let sid = SteamID::new();
537 assert!(!sid.is_valid());
538 }
539
540 #[test]
541 fn test_invalid_individual_instance() {
542 let sid: SteamID = "[U:1:46143802:10]".parse().unwrap();
543 assert!(!sid.is_valid());
544 assert!(!sid.is_valid_individual());
545 }
546
547 #[test]
548 fn test_invalid_non_all_clan_instance() {
549 let sid: SteamID = "[g:1:4681548:2]".parse().unwrap();
550 assert!(!sid.is_valid());
551 }
552
553 #[test]
554 fn test_invalid_gameserver_accountid_0() {
555 let sid: SteamID = "[G:1:0]".parse().unwrap();
556 assert!(!sid.is_valid());
557 }
558
559 #[test]
560 fn test_is_group_chat() {
561 let mut sid = SteamID::new();
562 sid.account_type = AccountType::Chat;
563 sid.instance = chat_instance_flags::CLAN;
564 assert!(sid.is_group_chat());
565 assert!(!sid.is_lobby());
566 }
567
568 #[test]
569 fn test_is_lobby() {
570 let mut sid = SteamID::new();
571 sid.account_type = AccountType::Chat;
572 sid.instance = chat_instance_flags::LOBBY;
573 assert!(!sid.is_group_chat());
574 assert!(sid.is_lobby());
575 }
576}