use std::any::{Any, TypeId};
use std::fmt;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::{Arc, Weak};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use async_trait::async_trait;
use parking_lot::RwLock;
use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;
use crate::base::client_msg::IClientMsg;
use crate::base::generated::steam_language::EMsg;
use crate::base::packet_base::IPacketMsg;
use crate::error::SteamError;
use crate::steam::authentication::steam_authentication::SteamAuthentication;
use crate::steam::cm_client::{CMClient, ClientMsgDispatcher, ConnectionFactory};
use crate::steam::discovery::server_record::ServerRecord;
use crate::steam::discovery::smart_cm_server_list::SmartCMServerList;
use crate::steam::handlers::client_msg_handler::ClientMsgHandler;
use crate::steam::handlers::steam_apps::steam_apps::SteamApps;
use crate::steam::handlers::steam_auth_ticket::steam_auth_ticket::SteamAuthTicket;
use crate::steam::handlers::steam_cloud::steam_cloud::SteamCloud;
use crate::steam::handlers::steam_content::steam_content::SteamContent;
use crate::steam::handlers::steam_friends::steam_friends::SteamFriends;
use crate::steam::handlers::steam_game_coordinator::steam_game_coordinator::SteamGameCoordinator;
use crate::steam::handlers::steam_game_server::steam_game_server::SteamGameServer;
use crate::steam::handlers::steam_master_server::steam_master_server::SteamMasterServer;
use crate::steam::handlers::steam_matchmaking::steam_matchmaking::SteamMatchmaking;
use crate::steam::handlers::steam_networking::steam_networking::SteamNetworking;
use crate::steam::handlers::steam_screenshots::steam_screenshots::SteamScreenshots;
use crate::steam::handlers::steam_unified_messages::steam_unified_messages::SteamUnifiedMessages;
use crate::steam::handlers::steam_user::steam_user::SteamUser;
use crate::steam::handlers::steam_user_stats::steam_user_stats::SteamUserStats;
use crate::steam::handlers::steam_workshop::steam_workshop::SteamWorkshop;
use crate::steam::steam_client::async_job_manager::AsyncJobManager;
use crate::steam::steam_client::callback_mgr::callback_msg::CallbackMsg;
use crate::steam::steam_client::callback_mgr::callback_mgr::{CallbackManager, SubscriptionId};
use crate::steam::steam_client::callbacks::{ConnectedCallback, DisconnectedCallback};
use crate::steam::steam_client::configuration::steam_configuration::SteamConfiguration;
use crate::types::job_id::{AsyncJob, JobID};
const GLOBAL_ID_EPOCH_SECONDS: u64 = 1_104_537_600;
struct HandlerEntry {
type_id: Option<TypeId>,
handler: Arc<dyn ClientMsgHandler>,
any: Option<Arc<dyn Any + Send + Sync>>,
}
struct SteamClientDispatcher {
client: Weak<SteamClient>,
}
#[async_trait]
impl ClientMsgDispatcher for SteamClientDispatcher {
async fn on_client_msg_received(&self, _client: &CMClient, packet: &dyn IPacketMsg) -> bool {
match self.client.upgrade() {
Some(client) => client.on_client_msg_received(packet).await,
None => false,
}
}
async fn on_client_connected(&self, _client: &CMClient) {
if let Some(client) = self.client.upgrade() {
client.on_client_connected().await;
}
}
async fn on_client_disconnected(&self, _client: &CMClient, user_initiated: bool) {
if let Some(client) = self.client.upgrade() {
client.on_client_disconnected(user_initiated).await;
}
}
}
pub struct SteamClient {
cm: Arc<CMClient>,
weak: Weak<SteamClient>,
handlers: RwLock<Vec<HandlerEntry>>,
current_job_id: AtomicU32,
process_start_time: u32,
callback_queue_tx: mpsc::UnboundedSender<Box<dyn CallbackMsg>>,
callback_queue_rx: tokio::sync::Mutex<mpsc::UnboundedReceiver<Box<dyn CallbackMsg>>>,
job_manager: AsyncJobManager,
callback_manager: CallbackManager,
authentication: std::sync::OnceLock<SteamAuthentication>,
}
impl fmt::Debug for SteamClient {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SteamClient")
.field("id", &self.cm.id())
.field("is_connected", &self.cm.is_connected())
.field("handlers", &self.handlers.read().len())
.finish()
}
}
impl SteamClient {
pub fn new() -> Arc<SteamClient> {
Self::with_configuration(Arc::new(SteamConfiguration::create_default()))
}
pub fn with_config(config: SteamConfiguration) -> Arc<SteamClient> {
Self::with_configuration(Arc::new(config))
}
pub fn with_identifier(identifier: impl Into<String>) -> Arc<SteamClient> {
Self::with_configuration_and_identifier(
Arc::new(SteamConfiguration::create_default()),
identifier,
)
}
pub fn with_configuration(configuration: Arc<SteamConfiguration>) -> Arc<SteamClient> {
Self::with_configuration_and_identifier(configuration, random_identifier())
}
pub fn with_configuration_and_identifier(
configuration: Arc<SteamConfiguration>,
identifier: impl Into<String>,
) -> Arc<SteamClient> {
let identifier = identifier.into();
let identifier = if identifier.is_empty() {
random_identifier()
} else {
identifier
};
crate::util::hardware_utils::HardwareUtils::init(configuration.machine_info_provider().clone());
let (callback_queue_tx, callback_queue_rx) = mpsc::unbounded_channel::<Box<dyn CallbackMsg>>();
let client = Arc::new_cyclic(|weak| {
let dispatcher: Arc<dyn ClientMsgDispatcher> =
Arc::new(SteamClientDispatcher { client: weak.clone() });
let cm = Arc::new(CMClient::new(configuration.clone(), &identifier, dispatcher));
SteamClient {
cm,
weak: weak.clone(),
handlers: RwLock::new(Vec::new()),
current_job_id: AtomicU32::new(0),
process_start_time: capture_process_start_time(),
callback_queue_tx,
callback_queue_rx: tokio::sync::Mutex::new(callback_queue_rx),
job_manager: AsyncJobManager::new(),
callback_manager: CallbackManager::new(),
authentication: std::sync::OnceLock::new(),
}
});
add_default_handlers(&client);
client
}
pub fn configuration(&self) -> &SteamConfiguration {
self.cm.configuration()
}
pub fn configuration_arc(&self) -> &Arc<SteamConfiguration> {
self.cm.configuration()
}
pub fn identifier(&self) -> &str {
self.cm.id()
}
pub fn id(&self) -> &str {
self.cm.id()
}
pub fn servers(&self) -> &Arc<SmartCMServerList> {
self.cm.servers()
}
pub fn local_ip(&self) -> Option<std::net::IpAddr> {
self.cm.local_ip()
}
pub fn current_end_point(&self) -> Option<std::net::SocketAddr> {
self.cm.current_end_point()
}
pub fn public_ip(&self) -> Option<std::net::IpAddr> {
self.cm.public_ip()
}
pub fn ip_country_code(&self) -> Option<String> {
self.cm.ip_country_code()
}
pub fn user_country_code(&self) -> Option<String> {
self.cm.user_country_code()
}
pub fn universe(&self) -> crate::base::generated::steam_language::EUniverse {
self.cm.universe()
}
pub fn is_connected(&self) -> bool {
self.cm.is_connected()
}
pub fn session_token(&self) -> u64 {
self.cm.session_token()
}
pub fn cell_id(&self) -> Option<u32> {
self.cm.cell_id()
}
pub fn session_id(&self) -> Option<i32> {
self.cm.session_id()
}
pub fn steam_id(&self) -> Option<crate::types::steam_id::SteamID> {
self.cm.steam_id()
}
pub fn connection_timeout(&self) -> Duration {
self.cm.connection_timeout()
}
pub fn debug_network_listener(
&self,
) -> Option<Arc<dyn crate::util::debug_network_listener::IDebugNetworkListener>> {
self.cm.debug_network_listener()
}
pub fn set_debug_network_listener(
&self,
listener: Option<Arc<dyn crate::util::debug_network_listener::IDebugNetworkListener>>,
) {
self.cm.set_debug_network_listener(listener);
}
pub fn expect_disconnection(&self) -> bool {
self.cm.expect_disconnection()
}
pub fn set_expect_disconnection(&self, expect: bool) {
self.cm.set_expect_disconnection(expect);
}
pub fn set_connection_factory(&self, factory: ConnectionFactory) {
self.cm.set_connection_factory(factory);
}
pub fn cm_client(&self) -> &Arc<CMClient> {
&self.cm
}
pub fn job_manager(&self) -> &AsyncJobManager {
&self.job_manager
}
pub fn callback_manager(&self) -> &CallbackManager {
&self.callback_manager
}
pub fn authentication(&self) -> &SteamAuthentication {
self.authentication
.get_or_init(|| SteamAuthentication::new(self.weak.clone()))
}
pub fn add_handler(&self, handler: Arc<dyn ClientMsgHandler>) {
let mut handlers = self.handlers.write();
if handlers.iter().any(|entry| Arc::ptr_eq(&entry.handler, &handler)) {
tracing::warn!("the handler is already registered");
return;
}
handlers.push(HandlerEntry {
type_id: None,
handler,
any: None,
});
}
pub fn add_typed_handler<T>(&self, handler: T) -> Result<(), SteamError>
where
T: ClientMsgHandler + 'static,
{
let type_id = TypeId::of::<T>();
if self.handlers.read().iter().any(|entry| entry.type_id == Some(type_id)) {
return Err(SteamError::InvalidArgument(format!(
"a handler of type \"{:?}\" is already registered",
type_id
)));
}
let handler: Arc<T> = Arc::new(handler);
let as_handler: Arc<dyn ClientMsgHandler> = handler.clone();
let as_any: Arc<dyn Any + Send + Sync> = handler;
self.handlers.write().push(HandlerEntry {
type_id: Some(type_id),
handler: as_handler,
any: Some(as_any),
});
Ok(())
}
pub fn remove_handler(&self, handler: &Arc<dyn ClientMsgHandler>) -> bool {
let mut handlers = self.handlers.write();
let before = handlers.len();
handlers.retain(|entry| !Arc::ptr_eq(&entry.handler, handler));
handlers.len() != before
}
pub fn remove_handler_type<T>(&self) -> bool
where
T: ClientMsgHandler + 'static,
{
let type_id = TypeId::of::<T>();
let mut handlers = self.handlers.write();
let before = handlers.len();
handlers.retain(|entry| entry.type_id != Some(type_id));
handlers.len() != before
}
pub fn get_handler<T>(&self) -> Option<Arc<T>>
where
T: ClientMsgHandler + 'static,
{
let type_id = TypeId::of::<T>();
let handlers = self.handlers.read();
let entry = handlers.iter().find(|entry| entry.type_id == Some(type_id))?;
entry.any.clone()?.downcast::<T>().ok()
}
pub fn get_required_handler<T>(&self) -> Result<Arc<T>, SteamError>
where
T: ClientMsgHandler + 'static,
{
self.get_handler::<T>().ok_or_else(|| {
SteamError::NotFound(format!(
"a handler of type \"{:?}\" is not registered",
TypeId::of::<T>()
))
})
}
pub fn get_callback(&self) -> Option<Box<dyn CallbackMsg>> {
let mut receiver = self.callback_queue_rx.try_lock().ok()?;
receiver.try_recv().ok()
}
pub async fn wait_for_callback(&self) -> Result<Box<dyn CallbackMsg>, SteamError> {
let mut receiver = self.callback_queue_rx.lock().await;
receiver.recv().await.ok_or(SteamError::Cancelled)
}
pub async fn wait_for_callback_timeout(&self, timeout: Duration) -> Option<Box<dyn CallbackMsg>> {
let mut receiver = self.callback_queue_rx.lock().await;
match tokio::time::timeout(timeout, receiver.recv()).await {
Ok(callback) => callback,
Err(_) => None,
}
}
pub async fn wait_for_callback_async(
&self,
cancellation: CancellationToken,
) -> Result<Box<dyn CallbackMsg>, SteamError> {
let mut receiver = self.callback_queue_rx.lock().await;
tokio::select! {
_ = cancellation.cancelled() => Err(SteamError::Cancelled),
callback = receiver.recv() => callback.ok_or(SteamError::Cancelled),
}
}
pub fn post_callback(&self, msg: Box<dyn CallbackMsg>) {
let job_id = msg.job_id();
self.job_manager.try_complete_job(job_id, &*msg);
let _ = self.callback_queue_tx.send(msg);
}
pub fn post_callback_typed<T>(&self, msg: T)
where
T: CallbackMsg + 'static,
{
self.post_callback(Box::new(msg));
}
pub fn run_callbacks(&self) -> bool {
match self.get_callback() {
Some(callback) => {
self.callback_manager.dispatch(callback);
true
}
None => false,
}
}
pub async fn run_wait_callbacks(&self) -> Result<(), SteamError> {
let callback = self.wait_for_callback().await?;
self.callback_manager.dispatch(callback);
Ok(())
}
pub async fn run_wait_callbacks_timeout(&self, timeout: Duration) -> bool {
match self.wait_for_callback_timeout(timeout).await {
Some(callback) => {
self.callback_manager.dispatch(callback);
true
}
None => false,
}
}
pub async fn run_wait_all_callbacks(&self, timeout: Duration) {
if !self.run_wait_callbacks_timeout(timeout).await {
return;
}
while self.run_callbacks() {}
}
pub fn subscribe<T>(&self, callback_func: Box<dyn Fn(&T) + Send + Sync>) -> SubscriptionId
where
T: CallbackMsg + 'static,
{
self.callback_manager.subscribe(callback_func)
}
pub fn subscribe_job<T>(
&self,
job_id: JobID,
callback_func: Box<dyn Fn(&T) + Send + Sync>,
) -> SubscriptionId
where
T: CallbackMsg + 'static,
{
self.callback_manager.subscribe_job(job_id, callback_func)
}
pub fn unsubscribe(&self, id: SubscriptionId) {
self.callback_manager.unsubscribe(id);
}
pub fn get_next_job_id(&self) -> JobID {
let sequence = self.current_job_id.fetch_add(1, Ordering::SeqCst).wrapping_add(1) & 0xF_FFFF;
let value = (u64::from(self.process_start_time) << 20) | u64::from(sequence);
JobID::from_value(value)
}
pub fn start_job<T>(&self, job_id: JobID) -> AsyncJob<T>
where
T: CallbackMsg + Clone + 'static,
{
let job = self.job_manager.start_job::<T>(job_id);
if !self.is_connected() {
self.job_manager.fail_job(job_id);
}
job
}
pub async fn connect(&self, cm_server: Option<ServerRecord>) -> Result<(), SteamError> {
self.cm.connect(cm_server).await
}
pub async fn disconnect(&self) {
self.cm.disconnect().await;
}
pub fn send(&self, msg: &dyn IClientMsg) -> Result<(), SteamError> {
self.cm.send(msg)
}
pub fn log_debug(&self, category: &str, message: &str) {
self.cm.log_debug(category, message);
}
pub async fn receive_test_packet_msg(&self, packet: &dyn IPacketMsg) -> bool {
self.on_client_msg_received(packet).await
}
async fn on_client_msg_received(&self, packet: &dyn IPacketMsg) -> bool {
match packet.msg_type() {
EMsg::JobHeartbeat => self.job_manager.heartbeat_job(packet.target_job_id()),
EMsg::DestJobFailed => self.job_manager.fail_job(packet.target_job_id()),
_ => {}
}
let handlers: Vec<Arc<dyn ClientMsgHandler>> =
self.handlers.read().iter().map(|entry| entry.handler.clone()).collect();
for handler in handlers {
if let Err(error) = handler.handle_msg(self, packet).await {
self.log_debug("SteamClient", &format!("Unhandled exception from handler: {}", error));
self.cm.disconnect_with(false).await;
return false;
}
}
true
}
async fn on_client_connected(&self) {
self.job_manager.set_timeouts_enabled(true);
self.post_callback_typed(ConnectedCallback::new());
}
async fn on_client_disconnected(&self, user_initiated: bool) {
self.post_callback_typed(DisconnectedCallback::new(user_initiated));
self.job_manager.cancel_pending_jobs();
self.job_manager.set_timeouts_enabled(false);
self.clear_handler_caches();
}
fn clear_handler_caches(&self) {
if let Some(handler) = self.get_handler::<SteamMatchmaking>() {
handler.clear_lobby_cache();
}
}
}
fn add_default_handlers(client: &Arc<SteamClient>) {
let _ = client.add_typed_handler(SteamFriends::new(client));
let _ = client.add_typed_handler(SteamUser::new(client));
let _ = client.add_typed_handler(SteamApps::new(client));
let _ = client.add_typed_handler(SteamGameCoordinator::new(client));
let _ = client.add_typed_handler(SteamGameServer::new(client));
let _ = client.add_typed_handler(SteamUserStats::new(client));
let _ = client.add_typed_handler(SteamMasterServer::new(client));
let _ = client.add_typed_handler(SteamCloud::new(client));
let _ = client.add_typed_handler(SteamWorkshop::new(client));
let _ = client.add_typed_handler(SteamUnifiedMessages::new(client));
let _ = client.add_typed_handler(SteamScreenshots::new(client));
let _ = client.add_typed_handler(SteamMatchmaking::new(client));
let _ = client.add_typed_handler(SteamNetworking::new(client));
let _ = client.add_typed_handler(SteamContent::new(client));
let _ = client.add_typed_handler(SteamAuthTicket::new(client));
}
fn random_identifier() -> String {
format!("{:032x}", rand::random::<u128>())
}
fn capture_process_start_time() -> u32 {
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default();
(now.as_secs().saturating_sub(GLOBAL_ID_EPOCH_SECONDS) as u32) & 0x3FFF_FFFF
}