Skip to content

Instantly share code, notes, and snippets.

@JohnPeel
Last active August 28, 2022 02:45
Show Gist options
  • Save JohnPeel/64bd766aef9ae3b7926e9cba2589341b to your computer and use it in GitHub Desktop.
Save JohnPeel/64bd766aef9ae3b7926e9cba2589341b to your computer and use it in GitHub Desktop.
#![no_std]
extern crate alloc;
use core::convert::Infallible;
use alloc::{fmt, format, string::String};
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Universe {
Individual = 0,
Public = 1,
Beta = 2,
Internal = 3,
Developer = 4,
ReleaseCandidate = 5,
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum AccountType {
Invalid = 0,
Individual = 1,
Multiseat = 2,
GameServer = 3,
AnonGameServer = 4,
Pending = 5,
ContentServer = 6,
Clan = 7,
Chat = 8,
P2pSuperSeeder = 9,
AnonUser = 10,
}
// TODO: Support Chat flags.
#[repr(u32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Instance {
All = 0,
Desktop = 1,
Console = 2,
Web = 3,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct AccountNumber(u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct AccountId(u32);
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SteamID(u64);
#[derive(Debug, Clone, Copy)]
pub struct InvalidUniverse(u8);
impl TryFrom<u8> for Universe {
type Error = InvalidUniverse;
fn try_from(value: u8) -> Result<Self, Self::Error> {
use Universe::*;
Ok(match value {
0 => Individual,
1 => Public,
2 => Beta,
3 => Internal,
4 => Developer,
5 => ReleaseCandidate,
_ => Err(InvalidUniverse(value))?,
})
}
}
impl From<Universe> for u8 {
fn from(value: Universe) -> Self {
use Universe::*;
match value {
Individual => 0,
Public => 1,
Beta => 2,
Internal => 3,
Developer => 4,
ReleaseCandidate => 5,
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum InvalidAccountType {
InvalidValue(u8),
InvalidChar(char),
}
impl TryFrom<u8> for AccountType {
type Error = InvalidAccountType;
fn try_from(value: u8) -> Result<Self, Self::Error> {
use AccountType::*;
Ok(match value {
0 => Invalid,
1 => Individual,
2 => Multiseat,
3 => GameServer,
4 => AnonGameServer,
5 => Pending,
6 => ContentServer,
7 => Clan,
8 => Chat,
9 => P2pSuperSeeder,
10 => AnonUser,
_ => Err(InvalidAccountType::InvalidValue(value))?,
})
}
}
impl From<AccountType> for u8 {
fn from(value: AccountType) -> Self {
use AccountType::*;
match value {
Invalid => 0,
Individual => 1,
Multiseat => 2,
GameServer => 3,
AnonGameServer => 4,
Pending => 5,
ContentServer => 6,
Clan => 7,
Chat => 8,
P2pSuperSeeder => 9,
AnonUser => 10,
}
}
}
impl TryFrom<char> for AccountType {
type Error = InvalidAccountType;
fn try_from(value: char) -> Result<Self, Self::Error> {
use AccountType::*;
Ok(match value {
'I' => Invalid,
'U' => Individual,
'M' => Multiseat,
'G' => GameServer,
'A' => AnonGameServer,
'P' => Pending,
'C' => ContentServer,
'g' => Clan,
'T' | 'L' | 'c' => Chat,
'a' => AnonUser,
_ => Err(InvalidAccountType::InvalidChar(value))?,
})
}
}
impl From<AccountType> for char {
fn from(value: AccountType) -> Self {
use AccountType::*;
match value {
Invalid => 'I',
Individual => 'U',
Multiseat => 'M',
GameServer => 'G',
AnonGameServer => 'A',
Pending => 'P',
ContentServer => 'C',
Clan => 'g',
Chat => 'T',
P2pSuperSeeder => {
unimplemented!("P2pSuperSeeder does not have a character representation")
}
AnonUser => 'a',
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct InvalidInstance(u32);
impl TryFrom<u32> for Instance {
type Error = InvalidInstance;
fn try_from(value: u32) -> Result<Self, Self::Error> {
use Instance::*;
Ok(match value {
0 => All,
1 => Desktop,
2 => Console,
3 => Web,
_ => Err(InvalidInstance(value))?,
})
}
}
impl From<Instance> for u32 {
fn from(value: Instance) -> Self {
use Instance::*;
match value {
All => 0,
Desktop => 1,
Console => 2,
Web => 3,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct InvalidAccountNumber(u32);
impl TryFrom<u32> for AccountNumber {
type Error = InvalidAccountNumber;
fn try_from(value: u32) -> Result<Self, Self::Error> {
if value > 0x7FFFFFFF {
Err(InvalidAccountNumber(value))?
}
Ok(AccountNumber(value))
}
}
impl From<AccountNumber> for u32 {
fn from(value: AccountNumber) -> Self {
value.0
}
}
impl From<u32> for AccountId {
fn from(value: u32) -> Self {
AccountId(value)
}
}
impl From<AccountId> for u32 {
fn from(value: AccountId) -> Self {
value.0
}
}
impl From<SteamID> for u64 {
fn from(value: SteamID) -> Self {
value.0
}
}
impl fmt::Debug for SteamID {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SteamID")
.field("value", &self.0)
.field("universe", &self.universe())
.field("type", &self.account_type())
.field("instance", &self.instance())
.field("account_number", &self.account_number())
.field("account_id", &self.account_id())
.finish()
}
}
impl fmt::Display for SteamID {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "SteamID({})", &self.0)
}
}
#[derive(Debug)]
pub enum InvalidSteamID {
InvalidUniverse(InvalidUniverse),
InvalidAccountType(InvalidAccountType),
InvalidInstance(InvalidInstance),
InvalidAccountNumber(InvalidAccountNumber),
InvalidFormat,
ParseIntError(core::num::ParseIntError),
}
impl From<InvalidUniverse> for InvalidSteamID {
fn from(value: InvalidUniverse) -> Self {
InvalidSteamID::InvalidUniverse(value)
}
}
impl From<InvalidAccountType> for InvalidSteamID {
fn from(value: InvalidAccountType) -> Self {
InvalidSteamID::InvalidAccountType(value)
}
}
impl From<InvalidInstance> for InvalidSteamID {
fn from(value: InvalidInstance) -> Self {
InvalidSteamID::InvalidInstance(value)
}
}
impl From<InvalidAccountNumber> for InvalidSteamID {
fn from(value: InvalidAccountNumber) -> Self {
InvalidSteamID::InvalidAccountNumber(value)
}
}
impl From<core::num::ParseIntError> for InvalidSteamID {
fn from(value: core::num::ParseIntError) -> Self {
InvalidSteamID::ParseIntError(value)
}
}
impl From<Infallible> for InvalidSteamID {
fn from(_: Infallible) -> Self {
unreachable!()
}
}
impl AccountId {
pub fn account_number(&self) -> Option<AccountNumber> {
((self.0 >> 1) as u32).try_into().ok()
}
}
impl SteamID {
pub fn universe(&self) -> Result<Universe, InvalidUniverse> {
(((self.0 >> 56) & 0xFF) as u8).try_into()
}
pub fn account_type(&self) -> Result<AccountType, InvalidAccountType> {
(((self.0 >> 52) & 0xF) as u8).try_into()
}
pub fn instance(&self) -> Result<Instance, InvalidInstance> {
(((self.0 >> 32) & 0xFFFFF) as u32).try_into()
}
pub fn account_number(&self) -> Result<AccountNumber, InvalidAccountNumber> {
(((self.0 & 0xFFFFFFFF) >> 1) as u32).try_into()
}
pub fn account_id(&self) -> Result<AccountId, Infallible> {
((self.0 & 0xFFFFFFFF) as u32).try_into()
}
pub fn steam2id(&self) -> Result<String, InvalidSteamID> {
Ok(format!(
"STEAM_{}:{}:{}",
u8::from(self.universe()?),
self.0 & 0x1,
u32::from(self.account_number()?)
))
}
pub fn steam3id(&self) -> Result<String, InvalidSteamID> {
Ok(format!(
"[{}:{}:{}]",
char::from(self.account_type()?),
u8::from(self.universe()?),
u32::from(self.account_id()?)
))
}
pub fn parse_steam2id<S: AsRef<str>>(
value: S,
account_type: AccountType,
instance: Instance,
) -> Result<Self, InvalidSteamID> {
use InvalidSteamID::InvalidFormat;
let value = value.as_ref();
if !value.starts_with("STEAM_") {
return Err(InvalidFormat);
}
let mut parts = value[6..].split(':');
let universe = Universe::try_from(parts.next().ok_or(InvalidFormat)?.parse::<u8>()?)?;
let id = parts.next().ok_or(InvalidFormat)?.parse::<u8>()? as u64;
let account_number =
AccountNumber::try_from(parts.next().ok_or(InvalidFormat)?.parse::<u32>()?)?;
Ok(SteamID(
((u8::from(universe) as u64) << 56)
| (((u8::from(account_type) & 0xF) as u64) << 52)
| (((u32::from(instance) & 0x7FFF) as u64) << 32)
| ((u32::from(account_number) as u64) << 0x1)
| (id & 0x1),
))
}
pub fn parse_steam3id<S: AsRef<str>>(
value: S,
instance: Instance,
) -> Result<Self, InvalidSteamID> {
use InvalidSteamID::InvalidFormat;
let value = value.as_ref();
if !value.starts_with('[') || !value.ends_with(']') {
return Err(InvalidFormat);
}
let mut parts = value[1..value.len() - 1].split(':');
let account_type = parts.next().ok_or(InvalidFormat)?;
if account_type.len() != 1 {
return Err(InvalidFormat);
}
let account_type =
AccountType::try_from(account_type.chars().next().ok_or(InvalidFormat)?)?;
let universe = Universe::try_from(parts.next().ok_or(InvalidFormat)?.parse::<u8>()?)?;
let account_id = AccountId::try_from(parts.next().ok_or(InvalidFormat)?.parse::<u32>()?)?;
let id = account_id.0 & 0x1;
let account_number = AccountNumber::try_from(account_id.0 >> 1)?;
Ok(SteamID(
((u8::from(universe) as u64) << 56)
| (((u8::from(account_type) & 0xF) as u64) << 52)
| (((u32::from(instance) & 0x7FFF) as u64) << 32)
| ((u32::from(account_number) as u64) << 0x1)
| (id as u64),
))
}
}
#[cfg(test)]
mod tests {
use alloc::format;
use super::{AccountType, Instance, SteamID};
#[test]
fn steamid_to_others() {
let steamid = SteamID(76561197999189721);
assert_eq!(steamid.steam2id().unwrap(), "STEAM_1:1:19461996");
assert_eq!(steamid.steam3id().unwrap(), "[U:1:38923993]");
}
#[test]
fn steamid_from_steam2id() {
let steamid = SteamID::parse_steam2id(
"STEAM_1:1:19461996",
AccountType::Individual,
Instance::Desktop,
)
.unwrap();
assert_eq!(steamid, SteamID(76561197999189721));
}
#[test]
fn steamid_from_steam3id() {
let steamid = SteamID::parse_steam3id("[U:1:38923993]", Instance::Desktop).unwrap();
assert_eq!(steamid, SteamID(76561197999189721));
}
#[test]
fn steamid_debug() {
let steamid = SteamID(76561197999189721);
assert_eq!(
format!("{:?}", steamid),
"SteamID { value: 76561197999189721, universe: Ok(Public), type: Ok(Individual), instance: Ok(Desktop), account_number: Ok(AccountNumber(19461996)), account_id: Ok(AccountId(38923993)) }"
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment