use std::{fmt, str::FromStr, sync::LazyLock};
use regex::Regex;
use crate::{
enums::{chat_instance_flags, masks, AccountType, Instance, Universe},
error::SteamIdError,
};
static STEAM2_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^STEAM_([0-5]):([0-1]):([0-9]+)$").unwrap());
static STEAM3_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\[([a-zA-Z]):([0-5]):([0-9]+)(:[0-9]+)?\]$").unwrap());
static STEAMID64_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\d+$").unwrap());
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SteamID {
pub universe: Universe,
pub account_type: AccountType,
pub instance: u32,
pub account_id: u32,
}
impl Default for SteamID {
fn default() -> Self {
Self::new()
}
}
impl SteamID {
pub fn new() -> Self {
SteamID {
universe: Universe::Invalid,
account_type: AccountType::Invalid,
instance: Instance::All as u32,
account_id: 0,
}
}
pub fn from_individual_account_id(account_id: u32) -> Self {
SteamID {
universe: Universe::Public,
account_type: AccountType::Individual,
instance: Instance::Desktop as u32,
account_id,
}
}
pub fn from_steam_id64(id: u64) -> Self {
let account_id = (id & masks::ACCOUNT_ID_MASK) as u32;
let instance = ((id >> 32) & masks::ACCOUNT_INSTANCE_MASK as u64) as u32;
let account_type = AccountType::from_u8(((id >> 52) & 0xF) as u8);
let universe = Universe::from_u8((id >> 56) as u8);
SteamID { universe, account_type, instance, account_id }
}
pub fn is_valid(&self) -> bool {
if self.account_type == AccountType::Invalid || (self.account_type as u8) > 10 {
return false;
}
if self.universe == Universe::Invalid || (self.universe as u8) > 4 {
return false;
}
if self.account_type == AccountType::Individual && (self.account_id == 0 || self.instance > Instance::Web as u32) {
return false;
}
if self.account_type == AccountType::Clan && (self.account_id == 0 || self.instance != Instance::All as u32) {
return false;
}
if self.account_type == AccountType::GameServer && self.account_id == 0 {
return false;
}
true
}
pub fn is_valid_individual(&self) -> bool {
self.universe == Universe::Public && self.account_type == AccountType::Individual && self.instance == Instance::Desktop as u32 && self.is_valid()
}
pub fn is_group_chat(&self) -> bool {
self.account_type == AccountType::Chat && (self.instance & chat_instance_flags::CLAN) != 0
}
pub fn is_lobby(&self) -> bool {
self.account_type == AccountType::Chat && ((self.instance & chat_instance_flags::LOBBY) != 0 || (self.instance & chat_instance_flags::MMS_LOBBY) != 0)
}
pub fn steam2(&self, newer_format: bool) -> Result<String, SteamIdError> {
if self.account_type != AccountType::Individual {
return Err(SteamIdError::NotIndividual);
}
let universe = if !newer_format && self.universe == Universe::Public { 0 } else { self.universe as u8 };
Ok(format!("STEAM_{}:{}:{}", universe, self.account_id & 1, self.account_id / 2))
}
pub fn steam3(&self) -> String {
let mut type_char = self.account_type.to_char();
if (self.instance & chat_instance_flags::CLAN) != 0 {
type_char = 'c';
} else if (self.instance & chat_instance_flags::LOBBY) != 0 {
type_char = 'L';
}
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);
if should_render_instance {
format!("[{}:{}:{}:{}]", type_char, self.universe as u8, self.account_id, self.instance)
} else {
format!("[{}:{}:{}]", type_char, self.universe as u8, self.account_id)
}
}
pub fn steam_id64(&self) -> u64 {
let universe = (self.universe as u64) << 56;
let account_type = (self.account_type as u64) << 52;
let instance = (self.instance as u64) << 32;
let account_id = self.account_id as u64;
universe | account_type | instance | account_id
}
fn parse_steam2(input: &str) -> Option<Self> {
let caps = STEAM2_REGEX.captures(input)?;
let universe_num: u8 = caps.get(1)?.as_str().parse().ok()?;
let mod_num: u32 = caps.get(2)?.as_str().parse().ok()?;
let account_id_half: u32 = caps.get(3)?.as_str().parse().ok()?;
let universe = if universe_num == 0 { Universe::Public } else { Universe::from_u8(universe_num) };
Some(SteamID {
universe,
account_type: AccountType::Individual,
instance: Instance::Desktop as u32,
account_id: (account_id_half * 2) + mod_num,
})
}
fn parse_steam3(input: &str) -> Option<Self> {
let caps = STEAM3_REGEX.captures(input)?;
let type_char = caps.get(1)?.as_str().chars().next()?;
let universe_num: u8 = caps.get(2)?.as_str().parse().ok()?;
let account_id: u32 = caps.get(3)?.as_str().parse().ok()?;
let universe = Universe::from_u8(universe_num);
let mut instance: u32 = Instance::All as u32;
if let Some(instance_match) = caps.get(4) {
let instance_str = &instance_match.as_str()[1..];
instance = instance_str.parse().ok()?;
}
let account_type = match type_char {
'U' => {
if caps.get(4).is_none() {
instance = Instance::Desktop as u32;
}
AccountType::Individual
}
'c' => {
instance |= chat_instance_flags::CLAN;
AccountType::Chat
}
'L' => {
instance |= chat_instance_flags::LOBBY;
AccountType::Chat
}
_ => AccountType::from_char(type_char),
};
Some(SteamID { universe, account_type, instance, account_id })
}
fn parse_steam_id64(input: &str) -> Option<Self> {
if !STEAMID64_REGEX.is_match(input) {
return None;
}
let id: u64 = input.parse().ok()?;
Some(Self::from_steam_id64(id))
}
}
impl FromStr for SteamID {
type Err = SteamIdError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
if let Some(sid) = Self::parse_steam2(input) {
return Ok(sid);
}
if let Some(sid) = Self::parse_steam3(input) {
return Ok(sid);
}
if let Some(sid) = Self::parse_steam_id64(input) {
return Ok(sid);
}
Err(SteamIdError::InvalidFormat(input.to_string()))
}
}
impl TryFrom<&str> for SteamID {
type Error = SteamIdError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
value.parse()
}
}
impl TryFrom<String> for SteamID {
type Error = SteamIdError;
fn try_from(value: String) -> Result<Self, Self::Error> {
value.parse()
}
}
impl From<u64> for SteamID {
fn from(value: u64) -> Self {
Self::from_steam_id64(value)
}
}
impl fmt::Display for SteamID {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.steam_id64())
}
}
impl serde::Serialize for SteamID {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_u64(self.steam_id64())
}
}
impl<'de> serde::Deserialize<'de> for SteamID {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct SteamIDVisitor;
impl<'de> serde::de::Visitor<'de> for SteamIDVisitor {
type Value = SteamID;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a SteamID64 as a number or string")
}
fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(SteamID::from(value))
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
value.parse().map_err(serde::de::Error::custom)
}
}
deserializer.deserialize_any(SteamIDVisitor)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parameterless_construction() {
let sid = SteamID::new();
assert_eq!(sid.universe, Universe::Invalid);
assert_eq!(sid.account_type, AccountType::Invalid);
assert_eq!(sid.instance, Instance::All as u32);
assert_eq!(sid.account_id, 0);
}
#[test]
fn test_from_individual_account_id() {
let sid = SteamID::from_individual_account_id(46143802);
assert_eq!(sid.universe, Universe::Public);
assert_eq!(sid.account_type, AccountType::Individual);
assert_eq!(sid.instance, Instance::Desktop as u32);
assert_eq!(sid.account_id, 46143802);
assert!(sid.is_valid());
assert!(sid.is_valid_individual());
}
#[test]
fn test_steam2id_construction_universe_0() {
let sid: SteamID = "STEAM_0:0:23071901".parse().unwrap();
assert_eq!(sid.universe, Universe::Public);
assert_eq!(sid.account_type, AccountType::Individual);
assert_eq!(sid.instance, Instance::Desktop as u32);
assert_eq!(sid.account_id, 46143802);
}
#[test]
fn test_steam2id_construction_universe_1() {
let sid: SteamID = "STEAM_1:1:23071901".parse().unwrap();
assert_eq!(sid.universe, Universe::Public);
assert_eq!(sid.account_type, AccountType::Individual);
assert_eq!(sid.instance, Instance::Desktop as u32);
assert_eq!(sid.account_id, 46143803);
}
#[test]
fn test_steam3id_construction_individual() {
let sid: SteamID = "[U:1:46143802]".parse().unwrap();
assert_eq!(sid.universe, Universe::Public);
assert_eq!(sid.account_type, AccountType::Individual);
assert_eq!(sid.instance, Instance::Desktop as u32);
assert_eq!(sid.account_id, 46143802);
}
#[test]
fn test_steam3id_construction_gameserver() {
let sid: SteamID = "[G:1:31]".parse().unwrap();
assert_eq!(sid.universe, Universe::Public);
assert_eq!(sid.account_type, AccountType::GameServer);
assert_eq!(sid.instance, Instance::All as u32);
assert_eq!(sid.account_id, 31);
assert!(sid.is_valid());
assert!(!sid.is_valid_individual());
}
#[test]
fn test_steam3id_construction_anon_gameserver() {
let sid: SteamID = "[A:1:46124:11245]".parse().unwrap();
assert_eq!(sid.universe, Universe::Public);
assert_eq!(sid.account_type, AccountType::AnonGameServer);
assert_eq!(sid.instance, 11245);
assert_eq!(sid.account_id, 46124);
}
#[test]
fn test_steam3id_construction_lobby() {
let sid: SteamID = "[L:1:12345]".parse().unwrap();
assert_eq!(sid.universe, Universe::Public);
assert_eq!(sid.account_type, AccountType::Chat);
assert_eq!(sid.instance, chat_instance_flags::LOBBY);
assert_eq!(sid.account_id, 12345);
}
#[test]
fn test_steam3id_construction_lobby_with_instanceid() {
let sid: SteamID = "[L:1:12345:55]".parse().unwrap();
assert_eq!(sid.universe, Universe::Public);
assert_eq!(sid.account_type, AccountType::Chat);
assert_eq!(sid.instance, chat_instance_flags::LOBBY | 55);
assert_eq!(sid.account_id, 12345);
}
#[test]
fn test_steamid64_construction_individual() {
let sid: SteamID = "76561198006409530".parse().unwrap();
assert_eq!(sid.universe, Universe::Public);
assert_eq!(sid.account_type, AccountType::Individual);
assert_eq!(sid.instance, Instance::Desktop as u32);
assert_eq!(sid.account_id, 46143802);
}
#[test]
fn test_steamid64_construction_clan() {
let sid: SteamID = "103582791434202956".parse().unwrap();
assert_eq!(sid.universe, Universe::Public);
assert_eq!(sid.account_type, AccountType::Clan);
assert_eq!(sid.instance, Instance::All as u32);
assert_eq!(sid.account_id, 4681548);
}
#[test]
fn test_steamid64_from_u64() {
let sid = SteamID::from(76561198006409530u64);
assert_eq!(sid.universe, Universe::Public);
assert_eq!(sid.account_type, AccountType::Individual);
assert_eq!(sid.instance, Instance::Desktop as u32);
assert_eq!(sid.account_id, 46143802);
}
#[test]
fn test_invalid_construction() {
let result: Result<SteamID, _> = "invalid input".parse();
assert!(result.is_err());
}
#[test]
fn test_steam2id_rendering_universe_0() {
let mut sid = SteamID::new();
sid.universe = Universe::Public;
sid.account_type = AccountType::Individual;
sid.instance = Instance::Desktop as u32;
sid.account_id = 46143802;
assert_eq!(sid.steam2(false).unwrap(), "STEAM_0:0:23071901");
}
#[test]
fn test_steam2id_rendering_universe_1() {
let mut sid = SteamID::new();
sid.universe = Universe::Public;
sid.account_type = AccountType::Individual;
sid.instance = Instance::Desktop as u32;
sid.account_id = 46143802;
assert_eq!(sid.steam2(true).unwrap(), "STEAM_1:0:23071901");
}
#[test]
fn test_steam2id_rendering_non_individual() {
let mut sid = SteamID::new();
sid.universe = Universe::Public;
sid.account_type = AccountType::Clan;
sid.instance = Instance::Desktop as u32;
sid.account_id = 4681548;
assert!(sid.steam2(false).is_err());
}
#[test]
fn test_steam3id_rendering_individual() {
let mut sid = SteamID::new();
sid.universe = Universe::Public;
sid.account_type = AccountType::Individual;
sid.instance = Instance::Desktop as u32;
sid.account_id = 46143802;
assert_eq!(sid.steam3(), "[U:1:46143802]");
}
#[test]
fn test_steam3id_rendering_anon_gameserver() {
let mut sid = SteamID::new();
sid.universe = Universe::Public;
sid.account_type = AccountType::AnonGameServer;
sid.instance = 41511;
sid.account_id = 43253156;
assert_eq!(sid.steam3(), "[A:1:43253156:41511]");
}
#[test]
fn test_steam3id_rendering_lobby() {
let mut sid = SteamID::new();
sid.universe = Universe::Public;
sid.account_type = AccountType::Chat;
sid.instance = chat_instance_flags::LOBBY;
sid.account_id = 451932;
assert_eq!(sid.steam3(), "[L:1:451932]");
}
#[test]
fn test_steamid64_rendering_individual() {
let mut sid = SteamID::new();
sid.universe = Universe::Public;
sid.account_type = AccountType::Individual;
sid.instance = Instance::Desktop as u32;
sid.account_id = 46143802;
assert_eq!(sid.steam_id64(), 76561198006409530);
assert_eq!(sid.to_string(), "76561198006409530");
}
#[test]
fn test_steamid64_rendering_anon_gameserver() {
let mut sid = SteamID::new();
sid.universe = Universe::Public;
sid.account_type = AccountType::AnonGameServer;
sid.instance = 188991;
sid.account_id = 42135013;
assert_eq!(sid.steam_id64(), 90883702753783269);
}
#[test]
fn test_invalid_new_id() {
let sid = SteamID::new();
assert!(!sid.is_valid());
}
#[test]
fn test_invalid_individual_instance() {
let sid: SteamID = "[U:1:46143802:10]".parse().unwrap();
assert!(!sid.is_valid());
assert!(!sid.is_valid_individual());
}
#[test]
fn test_invalid_non_all_clan_instance() {
let sid: SteamID = "[g:1:4681548:2]".parse().unwrap();
assert!(!sid.is_valid());
}
#[test]
fn test_invalid_gameserver_accountid_0() {
let sid: SteamID = "[G:1:0]".parse().unwrap();
assert!(!sid.is_valid());
}
#[test]
fn test_is_group_chat() {
let mut sid = SteamID::new();
sid.account_type = AccountType::Chat;
sid.instance = chat_instance_flags::CLAN;
assert!(sid.is_group_chat());
assert!(!sid.is_lobby());
}
#[test]
fn test_is_lobby() {
let mut sid = SteamID::new();
sid.account_type = AccountType::Chat;
sid.instance = chat_instance_flags::LOBBY;
assert!(!sid.is_group_chat());
assert!(sid.is_lobby());
}
}