Initial commit
This commit is contained in:
parent
3ee50f9aff
commit
bd6dbebb31
20 changed files with 2644 additions and 0 deletions
8
core/Cargo.toml
Normal file
8
core/Cargo.toml
Normal 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
85
core/src/class.rs
Normal 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
5
core/src/lib.rs
Normal 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
264
core/src/steam_id.rs
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue