Initial commit

This commit is contained in:
Arne Dußin 2024-11-21 19:19:44 +01:00
parent 3ee50f9aff
commit bd6dbebb31
20 changed files with 2644 additions and 0 deletions

8
core/Cargo.toml Normal file
View file

@ -0,0 +1,8 @@
[package]
name = "ucore"
version = "0.1.0"
edition = "2021"
[dependencies]
num-derive = "0.4.2"
num-traits = "0.2.19"

85
core/src/class.rs Normal file
View file

@ -0,0 +1,85 @@
use std::fmt;
use std::str::FromStr;
use num_derive::FromPrimitive;
pub const NUM_CLASSES: usize = 9;
/// All TF2 classes.
#[derive(Copy, Clone, Debug, PartialEq, Eq, FromPrimitive)]
pub enum Class
{
Scout,
Soldier,
Pyro,
Demoman,
Heavy,
Engineer,
Medic,
Sniper,
Spy,
Unknown,
}
impl Class
{
/// Check if the class is considered a "main" class, which means it is under
/// consideration for main (most) played class during a game.
///
/// # Returns
/// `true` if the class is main-classeable, currently that includes
/// `Demoman`, `Scout`, `Soldier` and `Medic`. `false` for all other
/// classes.
pub fn is_main_class(self) -> bool
{
matches!(
self,
Self::Demoman | Self::Medic | Self::Scout | Self::Soldier
)
}
}
/// When creating identifying a class from a string, the class may be unknown in
/// case the string does not conform to all lowercase string as it is present in
/// the logs.tf API. In that case, this error is thrown, containing the content
/// of the string.
#[derive(Debug)]
pub struct UnknownClassError
{
class: String,
}
impl fmt::Display for UnknownClassError
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
{
write!(f, "Unknown class `{}`", &self.class)
}
}
impl std::error::Error for UnknownClassError {}
impl FromStr for Class
{
// Returns the name of the class in case the class is unknown
type Err = UnknownClassError;
fn from_str(s: &str) -> Result<Self, Self::Err>
{
match s {
"demoman" => Ok(Self::Demoman),
"engineer" => Ok(Self::Engineer),
"heavy" | "heavyweapons" => Ok(Self::Heavy),
"medic" => Ok(Self::Medic),
"pyro" => Ok(Self::Pyro),
"scout" => Ok(Self::Scout),
"sniper" => Ok(Self::Sniper),
"soldier" => Ok(Self::Soldier),
"spy" => Ok(Self::Spy),
"unknown" => Ok(Self::Unknown),
unknown => Err(UnknownClassError {
class: unknown.to_string(),
}),
}
}
}

5
core/src/lib.rs Normal file
View file

@ -0,0 +1,5 @@
pub mod class;
pub use class::*;
pub mod steam_id;
pub use steam_id::*;

264
core/src/steam_id.rs Normal file
View file

@ -0,0 +1,264 @@
//! Handling of steam ids. Since there are multiple versions and the logs.tf API
//! uses steamID64 for lookups but has steamID3s in the log files, a safe
//! conversion and type safety between these two is critical.
use std::str::FromStr;
use num_derive::FromPrimitive;
use num_traits::FromPrimitive;
const ACCOUNT_INSTANCE_OFFSET_BITS: u64 = 32;
// Account instance is located at ACCOUNT_INSTANCE_OFFSET_BITS and 20 bits long
const ACCOUNT_INSTANCE_MASK: u64 = 0xfffff << ACCOUNT_INSTANCE_OFFSET_BITS;
const ACCOUNT_TYPE_OFFSET_BITS: u64 = 52;
const UNIVERSE_OFFSET_BITS: u64 = 56;
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct SteamID
{
id64: u64,
}
impl SteamID
{
/// Create a steam id from its steamID64 representation.
///
/// The id value is not checked and it is therefore possible to create an
/// invalid steam id with this.
pub const unsafe fn new(id64: u64) -> Self { Self { id64 } }
/// Create a steam id from its steamID64 representation.
///
/// Checks if the id is in a sane format. In case it is not, an `Err(())` is
/// returned.
///
/// # Warning
/// It does not actually make a request to check if there is a profile
/// connected to this steam id, so lookups for the profile may still fail.
pub fn new_checked(id64: u64) -> Result<Self, ()>
{
if Self::try_for_universe(id64).is_some()
&& Self::try_for_account_type(id64).is_some()
// Check for normal user account, the only one currently supported
&& (id64 & ACCOUNT_INSTANCE_MASK == 1 << ACCOUNT_INSTANCE_OFFSET_BITS)
{
Ok(Self { id64 })
}
else {
Err(())
}
}
/// Create a steam id from the parts usually present. The account type will
/// always be set to a user account.
pub fn from_parts(universe: Universe, account_type: AccountType, id: u32) -> Self
{
let mut id64 = 0;
id64 |= id as u64;
// Assume user account
id64 |= 1 << ACCOUNT_INSTANCE_OFFSET_BITS;
id64 |= (account_type as u64) << ACCOUNT_TYPE_OFFSET_BITS;
id64 |= (universe as u64) << UNIVERSE_OFFSET_BITS;
Self { id64 }
}
fn try_for_universe(id64: u64) -> Option<Universe>
{
let universe_byte: u8 =
((id64 & (0xff << UNIVERSE_OFFSET_BITS)) >> UNIVERSE_OFFSET_BITS) as u8;
Universe::from_u8(universe_byte)
}
fn try_for_account_type(id64: u64) -> Option<AccountType>
{
let account_type_nibble: u8 =
((id64 & (0xf << ACCOUNT_TYPE_OFFSET_BITS)) >> ACCOUNT_TYPE_OFFSET_BITS) as u8;
AccountType::from_u8(account_type_nibble)
}
/// Get the universe this account is part of.
///
/// # Panics
/// If there is no valid universe in this steam id, which means that the
/// internal data is corrupt, which is only possible when creating using the
/// unsafe `new` method.
pub fn universe(self) -> Universe
{
Self::try_for_universe(self.id64).expect("Corrupted steam id. Check unsafe `new` calls")
}
/// Get the type of this account
///
/// # Panics
/// If the steam id is corrupt. Can only happen with accounts created with
/// unsafe `new` method.
pub fn account_type(self) -> AccountType
{
Self::try_for_account_type(self.id64).expect("Corrupted steam id. Check unsafe `new` calls")
}
pub fn id64(self) -> u64 { self.id64 }
pub fn to_id64_string(self) -> String { self.id64.to_string() }
pub fn to_id3_string(self) -> String
{
let mut res = "[".to_owned();
res.push(self.account_type().into());
res.push(':');
let id = self.id64 as u32;
res += &(id & 1).to_string();
res.push(':');
res += &(id >> 1).to_string();
res.push(']');
res
}
pub fn to_id1_string(self) -> String
{
let mut res = "STEAM_".to_owned();
res += &(self.universe() as u8).to_string();
res.push(':');
let id = self.id64 as u32;
res += &(id & 1).to_string();
res.push(':');
res += &(id >> 1).to_string();
res
}
}
impl FromStr for SteamID
{
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err>
{
// Try known conversions
// Starting with steamid64 if it's just a number.
if let Ok(id64) = s.parse::<u64>() {
Self::new_checked(id64)
}
// Check for ID3
else if s.starts_with('[') && s.ends_with(']') {
let parts: Vec<&str> = s.split(':').collect();
if parts.len() == 3 && parts[0].len() == 2 && parts[1].len() == 1 {
let account_id = parts[2][..parts[2].len() - 1]
.parse::<u32>()
.map_err(|_| ())?;
let account_type: AccountType = parts[0].chars().nth(1).unwrap().try_into()?;
let universe: Universe = Universe::Public;
Ok(Self::from_parts(universe, account_type, account_id))
}
else {
Err(())
}
}
// Check for legacy ID format
else if s.starts_with("STEAM_") {
todo!()
}
// Not a known format
else {
Err(())
}
}
}
#[derive(Copy, Clone, Debug, FromPrimitive)]
pub enum Universe
{
Unspecified = 0,
Public = 1,
Beta = 2,
Internal = 3,
Dev = 4,
RC = 5,
}
#[derive(Copy, Clone, Debug, FromPrimitive)]
pub enum AccountType
{
Invalid = 0,
Individual = 1,
Multiseat = 2,
GameServer = 3,
AnonGameServer = 4,
Pending = 5,
ContentServer = 6,
Clan = 7,
Chat = 8,
// P2P SuperSeeder ignored
AnonUser = 10,
}
impl From<AccountType> for char
{
fn from(value: AccountType) -> Self
{
match value {
AccountType::Invalid => 'I',
AccountType::Individual => 'U',
AccountType::Multiseat => 'M',
AccountType::GameServer => 'G',
AccountType::AnonGameServer => 'A',
AccountType::Pending => 'P',
AccountType::ContentServer => 'C',
AccountType::Clan => 'g',
AccountType::Chat => 'c',
AccountType::AnonUser => 'a',
}
}
}
impl TryFrom<char> for AccountType
{
type Error = ();
fn try_from(value: char) -> Result<Self, Self::Error>
{
match value {
'I' => Ok(Self::Invalid),
'U' => Ok(Self::Individual),
'M' => Ok(Self::Multiseat),
'G' => Ok(Self::GameServer),
'A' => Ok(Self::AnonGameServer),
'P' => Ok(Self::Pending),
'C' => Ok(Self::ContentServer),
'g' => Ok(Self::Clan),
'T' | 'L' | 'c' => Ok(Self::Chat),
'a' => Ok(Self::AnonUser),
_ => Err(()),
}
}
}
#[cfg(test)]
mod test
{
use std::str::FromStr;
use crate::SteamID;
#[test]
fn from_id3()
{
assert_eq!(
SteamID::from_str("[U:1:71020853]")
.expect("Unable to parse")
.id64(),
76561198031286581
);
assert_eq!(
SteamID::from_str("[U:1:287181528]")
.expect("Unable to parse")
.id64(),
76561198247447256
);
}
}