Initial commit
This commit is contained in:
parent
3ee50f9aff
commit
bd6dbebb31
20 changed files with 2644 additions and 0 deletions
1573
Cargo.lock
generated
Normal file
1573
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
9
Cargo.toml
Normal file
9
Cargo.toml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
[workspace]
|
||||||
|
resolver = "2"
|
||||||
|
|
||||||
|
members = [
|
||||||
|
"cli",
|
||||||
|
"core",
|
||||||
|
"db",
|
||||||
|
"logs_tf"
|
||||||
|
]
|
||||||
6
cli/Cargo.toml
Normal file
6
cli/Cargo.toml
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
[package]
|
||||||
|
name = "ucli"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
4
cli/src/main.rs
Normal file
4
cli/src/main.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
fn main()
|
||||||
|
{
|
||||||
|
println!("Hello, world!");
|
||||||
|
}
|
||||||
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
6
db/Cargo.toml
Normal file
6
db/Cargo.toml
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
[package]
|
||||||
|
name = "udb"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
14
db/src/lib.rs
Normal file
14
db/src/lib.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
pub fn add(left: u64, right: u64) -> u64 { left + right }
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests
|
||||||
|
{
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_works()
|
||||||
|
{
|
||||||
|
let result = add(2, 2);
|
||||||
|
assert_eq!(result, 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
logs_tf/Cargo.toml
Normal file
10
logs_tf/Cargo.toml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
[package]
|
||||||
|
name = "logs_tf"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
chrono = "0.4.38"
|
||||||
|
json = "0.12.4"
|
||||||
|
reqwest = { version = "0.12.9", features = ["blocking"] }
|
||||||
|
ucore = { path = "../core" }
|
||||||
83
logs_tf/src/lib.rs
Normal file
83
logs_tf/src/lib.rs
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
pub mod log;
|
||||||
|
pub mod performance;
|
||||||
|
pub mod query_error;
|
||||||
|
pub mod search_params;
|
||||||
|
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use json::JsonValue;
|
||||||
|
pub use log::*;
|
||||||
|
pub use query_error::*;
|
||||||
|
use reqwest::blocking as reqwest;
|
||||||
|
|
||||||
|
use self::search_params::SearchParams;
|
||||||
|
|
||||||
|
const LOGS_TF_API_BASE: &str = "https://logs.tf/api/v1/log";
|
||||||
|
|
||||||
|
/// Function that tries to execute something that returns a result. If it does
|
||||||
|
/// not work the first time, it will keep trying num_retries times until it
|
||||||
|
/// either returns Ok() or all the tries have been used up.
|
||||||
|
pub(self) fn keep_trying<A, R, E>(action: A, num_retries: u8) -> Result<R, E>
|
||||||
|
where
|
||||||
|
A: Fn() -> Result<R, E>,
|
||||||
|
{
|
||||||
|
let mut num_tries = 0;
|
||||||
|
loop {
|
||||||
|
let res = action();
|
||||||
|
num_tries += 1;
|
||||||
|
|
||||||
|
if res.is_ok() || num_tries > num_retries + 1 {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sleep for a little time before making a request to logs.tf. The API is very
|
||||||
|
/// sensitive to quickly making queries to it and will respond with invalid
|
||||||
|
/// responses otherwise.
|
||||||
|
pub(self) fn log_delay() { thread::sleep(Duration::from_millis(500)) }
|
||||||
|
|
||||||
|
/// Checks for the `"success": true` field in the json value, which is always
|
||||||
|
/// set by logs.tf. If `"success": false` is set, it will parse the error and
|
||||||
|
/// return a `QueryError`.
|
||||||
|
fn check_json_success(json: &JsonValue) -> QueryResult<()>
|
||||||
|
{
|
||||||
|
let success = json["success"].as_bool().unwrap();
|
||||||
|
|
||||||
|
if success {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let error = json["error"].as_str().unwrap();
|
||||||
|
Err(QueryError::Unsuccessful(error.to_owned()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn search_logs_once(search_params: &SearchParams) -> QueryResult<Vec<LogMetadata>>
|
||||||
|
{
|
||||||
|
log_delay();
|
||||||
|
|
||||||
|
let request = reqwest::Client::builder().build()?.get(LOGS_TF_API_BASE);
|
||||||
|
let request = search_params.add_params_to_request(request);
|
||||||
|
|
||||||
|
let response = request.send()?;
|
||||||
|
let json = json::parse(&(response.text()?)).unwrap();
|
||||||
|
check_json_success(&json)?;
|
||||||
|
|
||||||
|
Ok(json["logs"]
|
||||||
|
.members()
|
||||||
|
.map(|meta| LogMetadata::from_json(meta))
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Query logs.tf for logs with the given parameters. Takes a number of retries.
|
||||||
|
/// Should the first query fail, this is the number of tries it will take until
|
||||||
|
/// it gives up querying.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// The metadata of all logs that fit the search parameters
|
||||||
|
pub fn search_logs(search_params: SearchParams, num_retries: u8) -> QueryResult<Vec<LogMetadata>>
|
||||||
|
{
|
||||||
|
keep_trying(|| search_logs_once(&search_params), num_retries)
|
||||||
|
}
|
||||||
127
logs_tf/src/log.rs
Normal file
127
logs_tf/src/log.rs
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use json::JsonValue;
|
||||||
|
use ucore::SteamID;
|
||||||
|
|
||||||
|
use super::{keep_trying, QueryResult, LOGS_TF_API_BASE};
|
||||||
|
use crate::performance::{Performance, Score};
|
||||||
|
|
||||||
|
pub struct LogMetadata
|
||||||
|
{
|
||||||
|
pub id: u32,
|
||||||
|
pub date_time: DateTime<Utc>,
|
||||||
|
pub map: String,
|
||||||
|
pub num_players: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Log
|
||||||
|
{
|
||||||
|
meta: LogMetadata,
|
||||||
|
performances: HashMap<SteamID, Vec<Performance>>,
|
||||||
|
duration_secs: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LogMetadata
|
||||||
|
{
|
||||||
|
pub fn from_json(json: &JsonValue) -> Self
|
||||||
|
{
|
||||||
|
Self {
|
||||||
|
id: json["id"].as_u32().unwrap(),
|
||||||
|
date_time: DateTime::from_timestamp(json["date"].as_i64().unwrap(), 0)
|
||||||
|
.expect("Failed to parse datetime"),
|
||||||
|
map: json["map"].as_str().unwrap().to_owned(),
|
||||||
|
num_players: json["players"].as_u8().unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Log
|
||||||
|
{
|
||||||
|
pub(crate) fn new(meta: LogMetadata, duration_secs: u32) -> Self
|
||||||
|
{
|
||||||
|
Self {
|
||||||
|
meta,
|
||||||
|
performances: HashMap::new(),
|
||||||
|
duration_secs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn download_once(id: u32) -> QueryResult<Self>
|
||||||
|
{
|
||||||
|
let log = reqwest::blocking::get(format!("{}/{}", LOGS_TF_API_BASE, id))?
|
||||||
|
.text()
|
||||||
|
.expect("Unable to read response body");
|
||||||
|
|
||||||
|
let json = json::parse(&log)?;
|
||||||
|
super::check_json_success(&json)?;
|
||||||
|
|
||||||
|
Ok(Self::from_json(id, &json))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Download the log with the given id from logs.tf and turn it into a
|
||||||
|
/// format that can be processed by a rating system easily.
|
||||||
|
pub fn download(id: u32, num_retries: u8) -> QueryResult<Self>
|
||||||
|
{
|
||||||
|
keep_trying(|| Self::download_once(id), num_retries)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse the json information as found on logs.tf into a format easily
|
||||||
|
/// digestible by the rating system.
|
||||||
|
// XXX: Check presumed logs.tf json for any format deviances
|
||||||
|
pub fn from_json(id: u32, json: &JsonValue) -> Self
|
||||||
|
{
|
||||||
|
let info = &json["info"];
|
||||||
|
let duration_secs = info["total_length"]
|
||||||
|
.as_u32()
|
||||||
|
.expect("Duration is not an unsigned int");
|
||||||
|
let map = info["map"]
|
||||||
|
.as_str()
|
||||||
|
.expect("Unable to read map of log")
|
||||||
|
.to_owned();
|
||||||
|
let timestamp = info["date"]
|
||||||
|
.as_u32()
|
||||||
|
.expect("Unable to read date as Unix timestamp") as i64;
|
||||||
|
let date_time = DateTime::from_timestamp(timestamp, 0).expect("Invalid timestamp");
|
||||||
|
let num_players = json["names"].members().len() as u8;
|
||||||
|
|
||||||
|
let meta = LogMetadata {
|
||||||
|
id,
|
||||||
|
date_time,
|
||||||
|
map,
|
||||||
|
num_players,
|
||||||
|
};
|
||||||
|
|
||||||
|
let score = Score::from_json(json);
|
||||||
|
|
||||||
|
let mut performances = HashMap::new();
|
||||||
|
for (player_id, stats) in json["players"].entries() {
|
||||||
|
let player_id =
|
||||||
|
SteamID::from_str(player_id).expect("Player id is not a valid steam id");
|
||||||
|
|
||||||
|
let player_performances = Performance::extract_all_from_json(&score, stats);
|
||||||
|
performances.insert(player_id, player_performances);
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
meta,
|
||||||
|
performances,
|
||||||
|
duration_secs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn meta(&self) -> &LogMetadata { &self.meta }
|
||||||
|
pub fn duration_secs(&self) -> u32 { self.duration_secs }
|
||||||
|
pub fn performances(&self) -> &HashMap<SteamID, Vec<Performance>> { &self.performances }
|
||||||
|
|
||||||
|
pub(crate) fn add_performance(&mut self, player: SteamID, performance: Performance)
|
||||||
|
{
|
||||||
|
match self.performances.get_mut(&player) {
|
||||||
|
Some(perfs) => perfs.push(performance),
|
||||||
|
None => {
|
||||||
|
self.performances.insert(player, vec![performance]);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
80
logs_tf/src/performance/dm_performance.rs
Normal file
80
logs_tf/src/performance/dm_performance.rs
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use json::JsonValue;
|
||||||
|
use ucore::Class;
|
||||||
|
|
||||||
|
use super::Performance;
|
||||||
|
|
||||||
|
pub struct DMPerformance
|
||||||
|
{
|
||||||
|
pub class: Class,
|
||||||
|
pub kills: u8,
|
||||||
|
pub assists: u8,
|
||||||
|
pub deaths: u8,
|
||||||
|
pub damage: u32,
|
||||||
|
pub time_played_secs: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DMPerformance
|
||||||
|
{
|
||||||
|
pub fn extract_all_from_json(json: &JsonValue) -> Vec<Self>
|
||||||
|
{
|
||||||
|
json["class_stats"]
|
||||||
|
.members()
|
||||||
|
.map(|class_stats| Self {
|
||||||
|
class: Class::from_str(class_stats["type"].as_str().unwrap()).unwrap(),
|
||||||
|
kills: class_stats["kills"].as_u8().unwrap(),
|
||||||
|
assists: class_stats["assists"].as_u8().unwrap(),
|
||||||
|
deaths: class_stats["deaths"].as_u8().unwrap(),
|
||||||
|
damage: class_stats["dmg"].as_u32().unwrap(),
|
||||||
|
time_played_secs: class_stats["total_time"].as_u32().unwrap(),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DMPerformance> for Performance
|
||||||
|
{
|
||||||
|
fn from(value: DMPerformance) -> Self { Self::DM(value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests
|
||||||
|
{
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Read;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_all_from_json()
|
||||||
|
{
|
||||||
|
let mut json = String::new();
|
||||||
|
File::open("test_data/log_3094861.json")
|
||||||
|
.expect("Unable to open test file")
|
||||||
|
.read_to_string(&mut json)
|
||||||
|
.expect("Unable to read file to string");
|
||||||
|
let json = json::parse(&json).expect("Unable to parse json");
|
||||||
|
|
||||||
|
let perfs = DMPerformance::extract_all_from_json(&json["players"]["[U:1:886717065]"]);
|
||||||
|
|
||||||
|
assert_eq!(perfs.len(), 3);
|
||||||
|
let scout_perf = &perfs[0];
|
||||||
|
let engi_perf = &perfs[1];
|
||||||
|
let _pyro_perf = &perfs[2];
|
||||||
|
|
||||||
|
assert_eq!(scout_perf.class, Class::Scout);
|
||||||
|
assert_eq!(scout_perf.kills, 19);
|
||||||
|
assert_eq!(scout_perf.assists, 14);
|
||||||
|
assert_eq!(scout_perf.deaths, 16);
|
||||||
|
assert_eq!(scout_perf.damage, 6671);
|
||||||
|
assert_eq!(scout_perf.time_played_secs, 1618);
|
||||||
|
|
||||||
|
assert_eq!(engi_perf.class, Class::Engineer);
|
||||||
|
assert_eq!(engi_perf.kills, 0);
|
||||||
|
assert_eq!(engi_perf.assists, 2);
|
||||||
|
assert_eq!(engi_perf.deaths, 0);
|
||||||
|
assert_eq!(engi_perf.damage, 293);
|
||||||
|
assert_eq!(engi_perf.time_played_secs, 99);
|
||||||
|
}
|
||||||
|
}
|
||||||
76
logs_tf/src/performance/medic_performance.rs
Normal file
76
logs_tf/src/performance/medic_performance.rs
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use json::JsonValue;
|
||||||
|
use ucore::Class;
|
||||||
|
|
||||||
|
use super::Performance;
|
||||||
|
|
||||||
|
pub struct MedicPerformance
|
||||||
|
{
|
||||||
|
pub healing: u32,
|
||||||
|
pub average_uber_length_secs: f32,
|
||||||
|
pub num_ubers: u8,
|
||||||
|
pub num_drops: u8,
|
||||||
|
pub deaths: u8,
|
||||||
|
pub time_played_secs: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MedicPerformance
|
||||||
|
{
|
||||||
|
pub fn extract_from_json(json: &JsonValue) -> Option<Self>
|
||||||
|
{
|
||||||
|
let class_stats = json["class_stats"].members().find(|class_stats| {
|
||||||
|
Class::from_str(class_stats["type"].as_str().unwrap()).unwrap() == Class::Medic
|
||||||
|
});
|
||||||
|
|
||||||
|
if !json.has_key("medicstats") || class_stats.is_none() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let class_stats = class_stats.unwrap();
|
||||||
|
|
||||||
|
Some(Self {
|
||||||
|
healing: json["heal"].as_u32().unwrap_or(0),
|
||||||
|
average_uber_length_secs: json["medicstats"]["avg_uber_length"]
|
||||||
|
.as_f32()
|
||||||
|
.unwrap_or(0.0),
|
||||||
|
num_ubers: json["ubers"].as_u8().unwrap_or(0),
|
||||||
|
num_drops: json["drops"].as_u8().unwrap_or(0),
|
||||||
|
deaths: class_stats["deaths"].as_u8().unwrap_or(0),
|
||||||
|
time_played_secs: class_stats["total_time"].as_u32().unwrap_or(0),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<MedicPerformance> for Performance
|
||||||
|
{
|
||||||
|
fn from(value: MedicPerformance) -> Self { Self::Med(value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests
|
||||||
|
{
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Read;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_from_json()
|
||||||
|
{
|
||||||
|
let mut json = String::new();
|
||||||
|
File::open("test_data/log_3094861.json")
|
||||||
|
.expect("Unable to open test file")
|
||||||
|
.read_to_string(&mut json)
|
||||||
|
.expect("Unable to read file to string");
|
||||||
|
let json = json::parse(&json).expect("Unable to parse json");
|
||||||
|
|
||||||
|
let stats = MedicPerformance::extract_from_json(&json["players"]["[U:1:71020853]"])
|
||||||
|
.expect("Unable to find medic performance");
|
||||||
|
assert_eq!(stats.healing, 22732);
|
||||||
|
assert_eq!(stats.average_uber_length_secs, 6.875);
|
||||||
|
assert_eq!(stats.num_ubers, 12);
|
||||||
|
assert_eq!(stats.num_drops, 0);
|
||||||
|
assert_eq!(stats.deaths, 10);
|
||||||
|
assert_eq!(stats.time_played_secs, 1738);
|
||||||
|
}
|
||||||
|
}
|
||||||
44
logs_tf/src/performance/mod.rs
Normal file
44
logs_tf/src/performance/mod.rs
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
pub mod dm_performance;
|
||||||
|
pub mod medic_performance;
|
||||||
|
pub mod overall_performance;
|
||||||
|
pub mod score;
|
||||||
|
|
||||||
|
use dm_performance::DMPerformance;
|
||||||
|
use json::JsonValue;
|
||||||
|
use medic_performance::MedicPerformance;
|
||||||
|
use overall_performance::OverallPerformance;
|
||||||
|
|
||||||
|
pub use self::score::Score;
|
||||||
|
|
||||||
|
/// A `Performance` contains what a player has done in the course of a game. It
|
||||||
|
/// contains either a generic performance, where data is not available on a per
|
||||||
|
/// class basis and the specific performance with information of that class,
|
||||||
|
/// being either a DM class or the medic.
|
||||||
|
pub enum Performance
|
||||||
|
{
|
||||||
|
Overall(OverallPerformance),
|
||||||
|
DM(DMPerformance),
|
||||||
|
Med(MedicPerformance),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Performance
|
||||||
|
{
|
||||||
|
pub fn extract_all_from_json(score: &Score, json: &JsonValue) -> Vec<Performance>
|
||||||
|
{
|
||||||
|
let overall_performance = OverallPerformance::from_json(score, json);
|
||||||
|
let dm_performances = DMPerformance::extract_all_from_json(json);
|
||||||
|
let med_performance = MedicPerformance::extract_from_json(json);
|
||||||
|
|
||||||
|
let mut performances = vec![overall_performance.into()];
|
||||||
|
|
||||||
|
for dm_perf in dm_performances {
|
||||||
|
performances.push(dm_perf.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(med_performance) = med_performance {
|
||||||
|
performances.push(med_performance.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
performances
|
||||||
|
}
|
||||||
|
}
|
||||||
53
logs_tf/src/performance/overall_performance.rs
Normal file
53
logs_tf/src/performance/overall_performance.rs
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use json::JsonValue;
|
||||||
|
|
||||||
|
use super::score::{Score, Team};
|
||||||
|
use super::Performance;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct OverallPerformance
|
||||||
|
{
|
||||||
|
pub won_rounds: u8,
|
||||||
|
pub num_rounds: u8,
|
||||||
|
pub damage: u32,
|
||||||
|
pub damage_taken: u32,
|
||||||
|
pub kills: u8,
|
||||||
|
pub deaths: u8,
|
||||||
|
pub num_medkits: u16,
|
||||||
|
pub medkits_hp: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OverallPerformance
|
||||||
|
{
|
||||||
|
pub fn from_json(score: &Score, json: &JsonValue) -> Self
|
||||||
|
{
|
||||||
|
let team = Team::from_str(json["team"].as_str().unwrap()).unwrap();
|
||||||
|
let won_rounds = score.get_score(team);
|
||||||
|
let lost_rounds = score.get_score(team.other());
|
||||||
|
let num_rounds = won_rounds + lost_rounds;
|
||||||
|
|
||||||
|
let damage = json["dmg"].as_u32().unwrap_or(0);
|
||||||
|
let damage_taken = json["dt"].as_u32().unwrap_or(0);
|
||||||
|
let kills = json["kills"].as_u8().unwrap_or(0);
|
||||||
|
let deaths = json["deaths"].as_u8().unwrap_or(0);
|
||||||
|
let num_medkits = json["medkits"].as_u16().unwrap_or(0);
|
||||||
|
let medkits_hp = json["medkits_hp"].as_u32().unwrap_or(0);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
won_rounds,
|
||||||
|
num_rounds,
|
||||||
|
damage,
|
||||||
|
damage_taken,
|
||||||
|
kills,
|
||||||
|
deaths,
|
||||||
|
num_medkits,
|
||||||
|
medkits_hp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<OverallPerformance> for Performance
|
||||||
|
{
|
||||||
|
fn from(value: OverallPerformance) -> Self { Self::Overall(value) }
|
||||||
|
}
|
||||||
62
logs_tf/src/performance/score.rs
Normal file
62
logs_tf/src/performance/score.rs
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use json::JsonValue;
|
||||||
|
|
||||||
|
pub struct Score
|
||||||
|
{
|
||||||
|
red: u8,
|
||||||
|
blue: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum Team
|
||||||
|
{
|
||||||
|
Red,
|
||||||
|
Blue,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Score
|
||||||
|
{
|
||||||
|
pub fn new(red: u8, blue: u8) -> Self { Self { red, blue } }
|
||||||
|
|
||||||
|
pub fn from_json(json: &JsonValue) -> Self
|
||||||
|
{
|
||||||
|
let red = json["teams"]["Red"]["score"].as_u8().unwrap();
|
||||||
|
let blue = json["teams"]["Blue"]["score"].as_u8().unwrap();
|
||||||
|
|
||||||
|
Self { red, blue }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_score(&self, team: Team) -> u8
|
||||||
|
{
|
||||||
|
match team {
|
||||||
|
Team::Red => self.red,
|
||||||
|
Team::Blue => self.blue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Team
|
||||||
|
{
|
||||||
|
pub fn other(self) -> Self
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
Self::Red => Self::Blue,
|
||||||
|
Self::Blue => Self::Red,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Team
|
||||||
|
{
|
||||||
|
type Err = ();
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err>
|
||||||
|
{
|
||||||
|
match s.trim().to_lowercase().as_str() {
|
||||||
|
"red" => Ok(Self::Red),
|
||||||
|
"blue" => Ok(Self::Blue),
|
||||||
|
_ => Err(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
56
logs_tf/src/query_error.rs
Normal file
56
logs_tf/src/query_error.rs
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
use std::error::Error;
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use json::JsonError;
|
||||||
|
use reqwest::Error as HttpError;
|
||||||
|
|
||||||
|
/// Any error that may occur when querying data from logs.tf
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum QueryError
|
||||||
|
{
|
||||||
|
/// An error that can occur when the connection to logs.tf is unstable or
|
||||||
|
/// the service is down.
|
||||||
|
HttpResponse(HttpError),
|
||||||
|
/// If for whatever reason an invalid Json file is returned by logs.tf or it
|
||||||
|
/// is corrupted.
|
||||||
|
JsonParseError(JsonError),
|
||||||
|
/// The Json object returned always contains `"success": true` or
|
||||||
|
/// `"success": false` to let the other party know if the query succeeded.
|
||||||
|
/// If it is false, this error is returned.
|
||||||
|
Unsuccessful(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type QueryResult<T> = Result<T, QueryError>;
|
||||||
|
|
||||||
|
impl From<HttpError> for QueryError
|
||||||
|
{
|
||||||
|
fn from(e: HttpError) -> Self { Self::HttpResponse(e) }
|
||||||
|
}
|
||||||
|
impl From<JsonError> for QueryError
|
||||||
|
{
|
||||||
|
fn from(e: JsonError) -> Self { Self::JsonParseError(e) }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for QueryError
|
||||||
|
{
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
Self::HttpResponse(http_e) => {
|
||||||
|
write!(f, "An error occured contacting logs.tf: {}", http_e)
|
||||||
|
},
|
||||||
|
Self::JsonParseError(json_e) => {
|
||||||
|
write!(f, "logs.tf did not return valid json: {}", json_e)
|
||||||
|
},
|
||||||
|
Self::Unsuccessful(e) => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"logs.tf could not successfully complete the query: {}",
|
||||||
|
e
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for QueryError {}
|
||||||
79
logs_tf/src/search_params.rs
Normal file
79
logs_tf/src/search_params.rs
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
use std::cmp;
|
||||||
|
|
||||||
|
use reqwest::blocking::RequestBuilder;
|
||||||
|
use ucore::SteamID;
|
||||||
|
|
||||||
|
pub struct SearchParams
|
||||||
|
{
|
||||||
|
pub player_id: Option<SteamID>,
|
||||||
|
pub title: Option<String>,
|
||||||
|
pub limit: Option<u16>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SearchParams
|
||||||
|
{
|
||||||
|
pub fn player_id(id: SteamID) -> Self
|
||||||
|
{
|
||||||
|
Self {
|
||||||
|
player_id: Some(id),
|
||||||
|
title: None,
|
||||||
|
limit: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn log_title(title: String) -> Self
|
||||||
|
{
|
||||||
|
Self {
|
||||||
|
player_id: None,
|
||||||
|
title: Some(title),
|
||||||
|
limit: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn limit(limit: u16) -> Self
|
||||||
|
{
|
||||||
|
let limit = cmp::min(limit, 10000);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
player_id: None,
|
||||||
|
title: None,
|
||||||
|
limit: Some(limit),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_player_id(mut self, id: SteamID) -> Self
|
||||||
|
{
|
||||||
|
self.player_id.replace(id);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_log_title(mut self, title: String) -> Self
|
||||||
|
{
|
||||||
|
self.title.replace(title);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_limit(mut self, limit: u16) -> Self
|
||||||
|
{
|
||||||
|
self.limit.replace(cmp::min(limit, 10000));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_params_to_request(&self, request_builder: RequestBuilder) -> RequestBuilder
|
||||||
|
{
|
||||||
|
let request_builder = match &self.player_id {
|
||||||
|
Some(id) => request_builder.query(&[("player", &id.to_id64_string())]),
|
||||||
|
None => request_builder,
|
||||||
|
};
|
||||||
|
|
||||||
|
let request_builder = match &self.title {
|
||||||
|
Some(name) => request_builder.query(&[("title", &name)]),
|
||||||
|
None => request_builder,
|
||||||
|
};
|
||||||
|
|
||||||
|
match &self.limit {
|
||||||
|
Some(limit) => request_builder.query(&[("limit", &limit.to_string())]),
|
||||||
|
None => request_builder,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue