1use std::fmt;
4
5use crate::base::generated::steam_language::{EAccountType, EUniverse};
6use crate::error::SteamError;
7
8#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
10pub(crate) struct BitVector64 {
11 data: u64,
12}
13
14impl BitVector64 {
15 pub(crate) fn new(value: u64) -> Self {
16 BitVector64 { data: value }
17 }
18
19 pub(crate) fn data(&self) -> u64 {
20 self.data
21 }
22
23 pub(crate) fn set_data(&mut self, value: u64) {
24 self.data = value;
25 }
26
27 pub(crate) fn get(&self, bitoffset: u32, valuemask: u64) -> u64 {
29 self.data.wrapping_shr(bitoffset) & valuemask
30 }
31
32 pub(crate) fn set(&mut self, bitoffset: u32, valuemask: u64, value: u64) {
34 let shifted_mask = valuemask.wrapping_shl(bitoffset);
35 self.data = (self.data & !shifted_mask) | (value & valuemask).wrapping_shl(bitoffset);
36 }
37}
38
39#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
41#[repr(u32)]
42pub enum ChatInstanceFlags {
43 Clan = (SteamID::ACCOUNT_INSTANCE_MASK + 1) >> 1,
45 Lobby = (SteamID::ACCOUNT_INSTANCE_MASK + 1) >> 2,
47 MMSLobby = (SteamID::ACCOUNT_INSTANCE_MASK + 1) >> 3,
49}
50
51impl ChatInstanceFlags {
52 pub const fn value(self) -> u32 {
54 self as u32
55 }
56
57 pub const fn has_flag(self, other: ChatInstanceFlags) -> bool {
59 (self as u32) & (other as u32) == (other as u32)
60 }
61}
62
63impl std::ops::BitOr for ChatInstanceFlags {
64 type Output = u32;
65
66 fn bitor(self, rhs: ChatInstanceFlags) -> u32 {
67 (self as u32) | (rhs as u32)
68 }
69}
70
71const UNKNOWN_ACCOUNT_TYPE_CHAR: char = 'i';
73
74#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
76pub struct SteamID(u64);
77
78impl SteamID {
79 pub const ALL_INSTANCES: u32 = 0;
81 pub const DESKTOP_INSTANCE: u32 = 1;
83 pub const CONSOLE_INSTANCE: u32 = 2;
85 pub const WEB_INSTANCE: u32 = 4;
87
88 pub const ACCOUNT_ID_MASK: u32 = 0xFFFF_FFFF;
90 pub const ACCOUNT_INSTANCE_MASK: u32 = 0x000F_FFFF;
92
93 pub fn new() -> SteamID {
95 SteamID(0)
96 }
97
98 pub fn from_id(id: u64) -> SteamID {
100 SteamID(id)
101 }
102
103 pub fn from_parts(account_type: EAccountType, universe: EUniverse, account_id: u32) -> SteamID {
107 let mut sid = SteamID(0);
108 sid.set(account_id, universe, account_type);
109 sid
110 }
111
112 pub fn from_parts_instanced(
114 account_type: EAccountType,
115 universe: EUniverse,
116 account_id: u32,
117 instance: u32,
118 ) -> SteamID {
119 let mut sid = SteamID(0);
120 sid.instanced_set(account_id, instance, universe, account_type);
121 sid
122 }
123
124 pub fn set(&mut self, account_id: u32, universe: EUniverse, account_type: EAccountType) {
126 self.set_account_id(account_id);
127 self.set_account_universe(universe);
128 self.set_account_type(account_type);
129
130 if account_type == EAccountType::Clan || account_type == EAccountType::GameServer {
131 self.set_account_instance(Self::ALL_INSTANCES);
132 } else {
133 self.set_account_instance(Self::DESKTOP_INSTANCE);
134 }
135 }
136
137 pub fn instanced_set(
139 &mut self,
140 account_id: u32,
141 instance: u32,
142 universe: EUniverse,
143 account_type: EAccountType,
144 ) {
145 self.set_account_id(account_id);
146 self.set_account_universe(universe);
147 self.set_account_type(account_type);
148 self.set_account_instance(instance);
149 }
150
151 pub fn set_from_string(&mut self, steam_id: &str, universe: EUniverse) -> bool {
154 let Some((_universe_digit, auth_server, account_id)) = parse_steam2_id(steam_id) else {
155 return false;
156 };
157
158 self.set_account_universe(universe);
159 self.set_account_instance(1);
160 self.set_account_type(EAccountType::Individual);
161 self.set_account_id(account_id.wrapping_shl(1) | auth_server);
162
163 true
164 }
165
166 pub fn set_from_steam3_string(&mut self, steam_id: &str) -> bool {
169 let Some(parsed) = parse_steam3_id(steam_id) else {
170 return false;
171 };
172
173 let mut instance = match parsed.instance {
174 Some(instance) => instance,
175 None => match parsed.type_char {
176 'g' | 'T' | 'c' | 'L' => 0,
177 _ => 1,
178 },
179 };
180
181 match parsed.type_char {
182 'c' => {
183 instance |= ChatInstanceFlags::Clan.value();
184 self.set_account_type(EAccountType::Chat);
185 }
186 'L' => {
187 instance |= ChatInstanceFlags::Lobby.value();
188 self.set_account_type(EAccountType::Chat);
189 }
190 'i' => {
191 self.set_account_type(EAccountType::Invalid);
192 }
193 other => {
194 let account_type = account_type_from_char(other);
195 self.set_account_type(account_type);
196 }
197 }
198
199 self.set_account_universe(universe_from_value(parsed.universe));
200 self.set_account_instance(instance);
201 self.set_account_id(parsed.account_id);
202
203 true
204 }
205
206 pub fn set_from_uint64(&mut self, ul_steam_id: u64) {
208 self.0 = ul_steam_id;
209 }
210
211 pub fn convert_to_u64(&self) -> u64 {
213 self.0
214 }
215
216 pub fn get_static_account_key(&self) -> u64 {
218 ((self.account_universe_raw() as u64) << 56)
219 + ((self.account_type_raw() as u64) << 52)
220 + (self.account_id() as u64)
221 }
222
223 pub fn value(&self) -> u64 {
225 self.0
226 }
227
228 fn account_type_raw(&self) -> u32 {
231 ((self.0 >> 52) & 0xF) as u32
232 }
233
234 fn account_universe_raw(&self) -> u32 {
237 ((self.0 >> 56) & 0xFF) as u32
238 }
239
240 pub fn is_blank_anon_account(&self) -> bool {
242 self.account_id() == 0 && self.is_anon_account() && self.account_instance() == 0
243 }
244
245 pub fn is_game_server_account(&self) -> bool {
247 let account_type = self.account_type_raw();
248 account_type == EAccountType::GameServer.value() as u32
249 || account_type == EAccountType::AnonGameServer.value() as u32
250 }
251
252 pub fn is_persistent_game_server_account(&self) -> bool {
254 self.account_type_raw() == EAccountType::GameServer.value() as u32
255 }
256
257 pub fn is_anon_game_server_account(&self) -> bool {
259 self.account_type_raw() == EAccountType::AnonGameServer.value() as u32
260 }
261
262 pub fn is_content_server_account(&self) -> bool {
264 self.account_type_raw() == EAccountType::ContentServer.value() as u32
265 }
266
267 pub fn is_clan_account(&self) -> bool {
269 self.account_type_raw() == EAccountType::Clan.value() as u32
270 }
271
272 pub fn is_chat_account(&self) -> bool {
274 self.account_type_raw() == EAccountType::Chat.value() as u32
275 }
276
277 pub fn is_lobby(&self) -> bool {
279 self.is_chat_account() && (self.account_instance() & ChatInstanceFlags::Lobby.value()) > 0
280 }
281
282 pub fn is_individual_account(&self) -> bool {
284 let account_type = self.account_type_raw();
285 account_type == EAccountType::Individual.value() as u32
286 || account_type == EAccountType::ConsoleUser.value() as u32
287 }
288
289 pub fn is_anon_account(&self) -> bool {
291 let account_type = self.account_type_raw();
292 account_type == EAccountType::AnonUser.value() as u32
293 || account_type == EAccountType::AnonGameServer.value() as u32
294 }
295
296 pub fn is_anon_user_account(&self) -> bool {
298 self.account_type_raw() == EAccountType::AnonUser.value() as u32
299 }
300
301 pub fn is_console_user_account(&self) -> bool {
303 self.account_type_raw() == EAccountType::ConsoleUser.value() as u32
304 }
305
306 pub fn is_valid(&self) -> bool {
308 let account_type = self.account_type_raw();
309 if account_type <= EAccountType::Invalid.value() as u32
310 || account_type > EAccountType::AnonUser.value() as u32
311 {
312 return false;
313 }
314
315 let universe = self.account_universe_raw();
316 if universe <= EUniverse::Invalid.value() as u32 || universe > EUniverse::Dev.value() as u32 {
317 return false;
318 }
319
320 if account_type == EAccountType::Individual.value() as u32
321 && (self.account_id() == 0 || self.account_instance() > Self::WEB_INSTANCE)
322 {
323 return false;
324 }
325
326 if account_type == EAccountType::Clan.value() as u32
327 && (self.account_id() == 0 || self.account_instance() != 0)
328 {
329 return false;
330 }
331
332 if account_type == EAccountType::GameServer.value() as u32 && self.account_id() == 0 {
333 return false;
334 }
335
336 true
337 }
338
339 pub fn account_id(&self) -> u32 {
341 (self.0 & Self::ACCOUNT_ID_MASK as u64) as u32
342 }
343
344 pub fn set_account_id(&mut self, value: u32) {
346 let mask = Self::ACCOUNT_ID_MASK as u64;
347 self.0 = (self.0 & !mask) | (value as u64 & mask);
348 }
349
350 pub fn account_instance(&self) -> u32 {
352 ((self.0 >> 32) & Self::ACCOUNT_INSTANCE_MASK as u64) as u32
353 }
354
355 pub fn set_account_instance(&mut self, value: u32) {
357 let mask = (Self::ACCOUNT_INSTANCE_MASK as u64) << 32;
358 self.0 = (self.0 & !mask) | ((value as u64) & Self::ACCOUNT_INSTANCE_MASK as u64) << 32;
359 }
360
361 pub fn account_type(&self) -> EAccountType {
365 account_type_from_value(self.account_type_raw())
366 }
367
368 pub fn set_account_type(&mut self, value: EAccountType) {
370 let raw = (value.value() as u64) & 0xF;
371 self.0 = (self.0 & !(0xF_u64 << 52)) | (raw << 52);
372 }
373
374 pub fn account_universe(&self) -> EUniverse {
377 universe_from_value(self.account_universe_raw())
378 }
379
380 pub fn set_account_universe(&mut self, value: EUniverse) {
382 let raw = (value.value() as u64) & 0xFF;
383 self.0 = (self.0 & !(0xFF_u64 << 56)) | (raw << 56);
384 }
385
386 pub fn render(&self, steam3: bool) -> String {
388 if steam3 {
389 self.render_steam3()
390 } else {
391 self.render_steam2()
392 }
393 }
394
395 pub fn to_chat_id(&self) -> u64 {
399 let mut chat_id = self.0;
400 chat_id = (chat_id & !((Self::ACCOUNT_INSTANCE_MASK as u64) << 32))
401 | (ChatInstanceFlags::Clan.value() as u64) << 32;
402 chat_id = (chat_id & !(0xF_u64 << 52)) | ((EAccountType::Chat.value() as u64 & 0xF) << 52);
403 chat_id
404 }
405
406 pub fn try_to_chat_id(&self) -> Result<SteamID, SteamError> {
408 if !self.is_clan_account() {
409 return Err(SteamError::invalid_argument(
410 "Only Clan IDs can be converted to Chat IDs.",
411 ));
412 }
413
414 Ok(SteamID(self.to_chat_id()))
415 }
416
417 pub fn try_get_clan_id(&self) -> Option<SteamID> {
419 if self.is_chat_account() && self.account_instance() == ChatInstanceFlags::Clan.value() {
420 let mut group_id = SteamID(self.0);
421 group_id.set_account_type(EAccountType::Clan);
422 group_id.set_account_instance(0);
423 Some(group_id)
424 } else {
425 None
426 }
427 }
428
429 fn render_steam2(&self) -> String {
431 let account_type_raw = self.account_type_raw();
432
433 if account_type_raw == EAccountType::Invalid.value() as u32
434 || account_type_raw == EAccountType::Individual.value() as u32
435 {
436 let universe_digit = if self.account_universe_raw() <= EUniverse::Public.value() as u32
437 {
438 "0".to_string()
439 } else {
440 self.account_universe_raw().to_string()
441 };
442 format!(
443 "STEAM_{}:{}:{}",
444 universe_digit,
445 self.account_id() & 1,
446 self.account_id() >> 1
447 )
448 } else {
449 self.0.to_string()
452 }
453 }
454
455 fn render_steam3(&self) -> String {
457 let account_type_raw = self.account_type_raw();
458 let mut account_type_char = account_type_char(account_type_raw);
459
460 if account_type_raw == EAccountType::Chat.value() as u32 {
461 let instance = self.account_instance();
462 if instance & ChatInstanceFlags::Clan.value() != 0 {
463 account_type_char = 'c';
464 } else if instance & ChatInstanceFlags::Lobby.value() != 0 {
465 account_type_char = 'L';
466 }
467 }
468
469 let render_instance = match account_type_raw {
470 raw if raw == EAccountType::AnonGameServer.value() as u32
471 || raw == EAccountType::Multiseat.value() as u32 =>
472 {
473 true
474 }
475 raw if raw == EAccountType::Individual.value() as u32 => {
476 self.account_instance() != Self::DESKTOP_INSTANCE
477 }
478 _ => false,
479 };
480
481 if render_instance {
482 format!(
483 "[{}:{}:{}:{}]",
484 account_type_char,
485 self.account_universe_raw(),
486 self.account_id(),
487 self.account_instance()
488 )
489 } else {
490 format!(
491 "[{}:{}:{}]",
492 account_type_char,
493 self.account_universe_raw(),
494 self.account_id()
495 )
496 }
497 }
498}
499
500impl fmt::Display for SteamID {
501 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
502 f.write_str(&self.render(true))
503 }
504}
505
506impl fmt::Debug for SteamID {
507 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
508 fmt::Display::fmt(self, f)
509 }
510}
511
512impl From<SteamID> for u64 {
513 fn from(sid: SteamID) -> u64 {
514 sid.0
515 }
516}
517
518impl From<u64> for SteamID {
519 fn from(id: u64) -> SteamID {
520 SteamID(id)
521 }
522}
523
524fn account_type_from_value(raw: u32) -> EAccountType {
526 match raw {
527 0 => EAccountType::Invalid,
528 1 => EAccountType::Individual,
529 2 => EAccountType::Multiseat,
530 3 => EAccountType::GameServer,
531 4 => EAccountType::AnonGameServer,
532 5 => EAccountType::Pending,
533 6 => EAccountType::ContentServer,
534 7 => EAccountType::Clan,
535 8 => EAccountType::Chat,
536 9 => EAccountType::ConsoleUser,
537 10 => EAccountType::AnonUser,
538 _ => EAccountType::Invalid,
539 }
540}
541
542fn universe_from_value(raw: u32) -> EUniverse {
544 match raw {
545 0 => EUniverse::Invalid,
546 1 => EUniverse::Public,
547 2 => EUniverse::Beta,
548 3 => EUniverse::Internal,
549 4 => EUniverse::Dev,
550 _ => EUniverse::Invalid,
551 }
552}
553
554fn account_type_from_char(ch: char) -> EAccountType {
556 match ch {
557 'A' => EAccountType::AnonGameServer,
558 'G' => EAccountType::GameServer,
559 'M' => EAccountType::Multiseat,
560 'P' => EAccountType::Pending,
561 'C' => EAccountType::ContentServer,
562 'g' => EAccountType::Clan,
563 'T' => EAccountType::Chat,
564 'I' => EAccountType::Invalid,
565 'U' => EAccountType::Individual,
566 'a' => EAccountType::AnonUser,
567 _ => EAccountType::Invalid,
568 }
569}
570
571fn account_type_char(raw: u32) -> char {
574 match raw {
575 0 => 'I',
576 1 => 'U',
577 2 => 'M',
578 3 => 'G',
579 4 => 'A',
580 5 => 'P',
581 6 => 'C',
582 7 => 'g',
583 8 => 'T',
584 10 => 'a',
585 _ => UNKNOWN_ACCOUNT_TYPE_CHAR,
586 }
587}
588
589struct Steam3Parts {
591 type_char: char,
592 universe: u32,
593 account_id: u32,
594 instance: Option<u32>,
595}
596
597fn parse_steam2_id(text: &str) -> Option<(u32, u32, u32)> {
601 let bytes = text.as_bytes();
602 let mut search_from = 0usize;
603
604 while let Some(offset) = find_ignore_case(bytes, b"STEAM_", search_from) {
605 if let Some(parsed) = parse_steam2_at(bytes, offset + 6) {
606 return Some(parsed);
607 }
608 search_from = offset + 1;
609 }
610
611 None
612}
613
614fn parse_steam2_at(bytes: &[u8], mut index: usize) -> Option<(u32, u32, u32)> {
615 if index >= bytes.len() || !bytes[index].is_ascii_digit() || bytes[index] > b'4' {
616 return None;
617 }
618 let universe = (bytes[index] - b'0') as u32;
619 index += 1;
620
621 if index >= bytes.len() || bytes[index] != b':' {
622 return None;
623 }
624 index += 1;
625
626 if index >= bytes.len() || !bytes[index].is_ascii_digit() || bytes[index] > b'1' {
627 return None;
628 }
629 let auth_server = (bytes[index] - b'0') as u32;
630 index += 1;
631
632 if index >= bytes.len() || bytes[index] != b':' {
633 return None;
634 }
635 index += 1;
636
637 let digits_start = index;
638 while index < bytes.len() && bytes[index].is_ascii_digit() {
639 index += 1;
640 }
641 if index == digits_start {
642 return None;
643 }
644
645 let account_id = parse_u32(&bytes[digits_start..index])?;
646
647 Some((universe, auth_server, account_id))
648}
649
650fn parse_steam3_id(text: &str) -> Option<Steam3Parts> {
655 parse_steam3_with(text, false).or_else(|| parse_steam3_with(text, true))
656}
657
658fn parse_steam3_with(text: &str, fallback: bool) -> Option<Steam3Parts> {
659 let bytes = text.as_bytes();
660 let mut search_from = 0usize;
661
662 while let Some(offset) = bytes[search_from..]
663 .iter()
664 .position(|&b| b == b'[')
665 .map(|pos| pos + search_from)
666 {
667 if let Some(parsed) = parse_steam3_at(bytes, offset, fallback) {
668 return Some(parsed);
669 }
670 search_from = offset + 1;
671 }
672
673 None
674}
675
676fn parse_steam3_at(bytes: &[u8], mut index: usize, fallback: bool) -> Option<Steam3Parts> {
677 if index + 1 >= bytes.len() {
678 return None;
679 }
680
681 let type_char = bytes[index + 1] as char;
682 if !matches!(
683 type_char,
684 'A' | 'G' | 'M' | 'P' | 'C' | 'g' | 'c' | 'L' | 'T' | 'I' | 'U' | 'a' | 'i'
685 ) {
686 return None;
687 }
688 index += 2;
689
690 if index >= bytes.len() || bytes[index] != b':' {
691 return None;
692 }
693 index += 1;
694
695 if index >= bytes.len() || !bytes[index].is_ascii_digit() || bytes[index] > b'4' {
696 return None;
697 }
698 let universe = (bytes[index] - b'0') as u32;
699 index += 1;
700
701 if index >= bytes.len() || bytes[index] != b':' {
702 return None;
703 }
704 index += 1;
705
706 let digits_start = index;
707 let mut digits_end = index;
708 while digits_end < bytes.len() && bytes[digits_end].is_ascii_digit() {
709 digits_end += 1;
710 }
711 if digits_end == digits_start {
712 return None;
713 }
714
715 let mut account_end = digits_end;
718 while account_end > digits_start {
719 let Some(account_id) = parse_u32(&bytes[digits_start..account_end]) else {
720 return None;
721 };
722 match parse_steam3_tail(bytes, account_end, fallback) {
723 Some(instance) => {
724 return Some(Steam3Parts {
725 type_char,
726 universe,
727 account_id,
728 instance,
729 })
730 }
731 None => account_end -= 1,
732 }
733 }
734
735 None
736}
737
738fn parse_steam3_tail(bytes: &[u8], index: usize, fallback: bool) -> Option<Option<u32>> {
740 let separator = if fallback { b'(' } else { b':' };
741
742 if index < bytes.len() && bytes[index] == separator {
743 let mut cursor = index + 1;
744 let digits_start = cursor;
745 while cursor < bytes.len() && bytes[cursor].is_ascii_digit() {
746 cursor += 1;
747 }
748 if cursor > digits_start {
749 if let Some(instance) = parse_u32(&bytes[digits_start..cursor]) {
750 if fallback {
751 if cursor < bytes.len() && bytes[cursor] == b')' {
754 cursor += 1;
755 if cursor < bytes.len() && bytes[cursor] == b']' {
756 return Some(Some(instance));
757 }
758 }
759 } else if cursor < bytes.len() && bytes[cursor] == b']' {
760 return Some(Some(instance));
763 }
764 }
765 }
766 }
767
768 if index < bytes.len() && bytes[index] == b']' {
769 return Some(None);
770 }
771
772 None
773}
774
775fn parse_u32(digits: &[u8]) -> Option<u32> {
777 let text = std::str::from_utf8(digits).ok()?;
778 let value: u64 = text.parse().ok()?;
779 if value > u32::MAX as u64 {
780 return None;
781 }
782 Some(value as u32)
783}
784
785fn find_ignore_case(haystack: &[u8], needle: &[u8], from: usize) -> Option<usize> {
787 if needle.is_empty() || haystack.len() < needle.len() {
788 return None;
789 }
790
791 let last = haystack.len() - needle.len();
792 let mut index = from;
793 while index <= last {
794 if haystack[index..index + needle.len()]
795 .iter()
796 .zip(needle.iter())
797 .all(|(a, b)| a.eq_ignore_ascii_case(b))
798 {
799 return Some(index);
800 }
801 index += 1;
802 }
803
804 None
805}