use std::sync::{Arc, Mutex};
use steamworks::{
Client, FriendFlags, FriendState, SteamId,
networking_messages::NetworkingMessages,
networking_types::{NetworkingIdentity, SendFlags},
};
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum PersonaState {
Offline,
Online,
Busy,
Away,
Snooze,
LookingToTrade,
LookingToPlay,
}
impl From<FriendState> for PersonaState {
fn from(state: FriendState) -> Self {
match state {
FriendState::Offline => PersonaState::Offline,
FriendState::Online => PersonaState::Online,
FriendState::Busy => PersonaState::Busy,
FriendState::Away => PersonaState::Away,
FriendState::Snooze => PersonaState::Snooze,
FriendState::LookingToTrade => PersonaState::LookingToTrade,
FriendState::LookingToPlay => PersonaState::LookingToPlay,
}
}
}
impl PersonaState {
pub fn as_str(&self) -> &'static str {
match self {
PersonaState::Offline => "Offline",
PersonaState::Online => "Online",
PersonaState::Busy => "Busy",
PersonaState::Away => "Away",
PersonaState::Snooze => "Snooze",
PersonaState::LookingToTrade => "Looking to Trade",
PersonaState::LookingToPlay => "Looking to Play",
}
}
pub fn is_online(&self) -> bool {
!matches!(self, PersonaState::Offline)
}
}
#[derive(Clone)]
pub struct AchievementInfo {
pub api_name: String,
pub achieved: bool,
}
#[derive(Clone)]
pub struct StatInfo {
pub api_name: String,
pub value: StatValue,
}
#[derive(Clone, Copy)]
pub enum StatValue {
Int(i32),
Float(f32),
}
#[derive(Clone)]
pub struct FriendInfo {
pub name: String,
pub steam_id: steamworks::SteamId,
pub state: PersonaState,
}
#[derive(Clone)]
pub struct NetworkMessage {
pub sender_name: String,
pub sender_id: SteamId,
pub data: Vec<u8>,
pub channel: u32,
pub is_outgoing: bool,
}
#[derive(Clone)]
pub struct PendingSessionRequest {
pub remote_id: SteamId,
pub remote_name: String,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum SessionState {
#[default]
None,
Connecting,
Connected,
ClosedByPeer,
ProblemDetected,
Failed,
}
#[derive(Clone)]
pub struct ActiveSession {
pub steam_id: SteamId,
pub name: String,
pub state: SessionState,
}
pub struct SteamResources {
client: Option<Client>,
pub initialized: bool,
pub user_name: String,
pub user_id: Option<SteamId>,
pub app_id: u32,
pub stats_received: bool,
pub achievements: Vec<AchievementInfo>,
pub stats: Vec<StatInfo>,
pub friends: Vec<FriendInfo>,
pub initialization_error: Option<String>,
pub pending_session_requests: Arc<Mutex<Vec<PendingSessionRequest>>>,
pub received_messages: Vec<NetworkMessage>,
pub active_sessions: Vec<ActiveSession>,
networking_callbacks_registered: bool,
}
impl Default for SteamResources {
fn default() -> Self {
Self {
client: None,
initialized: false,
user_name: String::new(),
user_id: None,
app_id: 0,
stats_received: false,
achievements: Vec::new(),
stats: Vec::new(),
friends: Vec::new(),
initialization_error: None,
pending_session_requests: Arc::new(Mutex::new(Vec::new())),
received_messages: Vec::new(),
active_sessions: Vec::new(),
networking_callbacks_registered: false,
}
}
}
impl SteamResources {
pub fn initialize(&mut self) -> Result<(), String> {
match Client::init() {
Ok(client) => {
let user = client.user();
let utils = client.utils();
let friends_interface = client.friends();
self.user_id = Some(user.steam_id());
self.user_name = friends_interface.name();
self.app_id = utils.app_id().0;
self.client = Some(client);
self.initialized = true;
tracing::info!(
"Steam initialized successfully for user: {}",
self.user_name
);
tracing::info!("App ID: {}", self.app_id);
Ok(())
}
Err(error) => {
let error_msg = format!("Failed to initialize Steam: {error:?}");
tracing::error!("{}", error_msg);
self.initialization_error = Some(error_msg.clone());
Err(error_msg)
}
}
}
pub fn run_callbacks(&self) {
if let Some(client) = &self.client {
client.run_callbacks();
}
}
pub fn request_stats(&mut self) {
self.refresh_achievements();
}
pub fn refresh_achievements(&mut self) {
let Some(client) = &self.client else {
return;
};
let user_stats = client.user_stats();
let num_achievements = user_stats.get_num_achievements().unwrap_or(0);
if num_achievements == 0 {
return;
}
let Some(achievement_names) = user_stats.get_achievement_names() else {
return;
};
self.achievements.clear();
for name in achievement_names {
let achieved = user_stats.achievement(&name).get().unwrap_or(false);
self.achievements.push(AchievementInfo {
api_name: name,
achieved,
});
}
self.stats_received = true;
}
pub fn refresh_stats(&mut self, stat_names: &[&str]) {
let Some(client) = &self.client else {
return;
};
let user_stats = client.user_stats();
self.stats.clear();
for &name in stat_names {
if let Ok(value) = user_stats.get_stat_i32(name) {
self.stats.push(StatInfo {
api_name: name.to_string(),
value: StatValue::Int(value),
});
} else if let Ok(value) = user_stats.get_stat_f32(name) {
self.stats.push(StatInfo {
api_name: name.to_string(),
value: StatValue::Float(value),
});
}
}
}
pub fn unlock_achievement(&self, name: &str) -> Result<(), String> {
let Some(client) = &self.client else {
return Err("Steam not initialized".to_string());
};
let user_stats = client.user_stats();
user_stats
.achievement(name)
.set()
.map_err(|_| format!("Failed to unlock achievement: {name}"))?;
Ok(())
}
pub fn clear_achievement(&self, name: &str) -> Result<(), String> {
let Some(client) = &self.client else {
return Err("Steam not initialized".to_string());
};
let user_stats = client.user_stats();
user_stats
.achievement(name)
.clear()
.map_err(|_| format!("Failed to clear achievement: {name}"))?;
Ok(())
}
pub fn set_stat_int(&self, name: &str, value: i32) -> Result<(), String> {
let Some(client) = &self.client else {
return Err("Steam not initialized".to_string());
};
let user_stats = client.user_stats();
user_stats
.set_stat_i32(name, value)
.map_err(|_| format!("Failed to set stat: {name}"))?;
Ok(())
}
pub fn set_stat_float(&self, name: &str, value: f32) -> Result<(), String> {
let Some(client) = &self.client else {
return Err("Steam not initialized".to_string());
};
let user_stats = client.user_stats();
user_stats
.set_stat_f32(name, value)
.map_err(|_| format!("Failed to set stat: {name}"))?;
Ok(())
}
pub fn store_stats(&self) -> Result<(), String> {
let Some(client) = &self.client else {
return Err("Steam not initialized".to_string());
};
let user_stats = client.user_stats();
user_stats
.store_stats()
.map_err(|_| "Failed to store stats".to_string())?;
Ok(())
}
pub fn reset_all_stats(&self, achievements_too: bool) -> Result<(), String> {
let Some(client) = &self.client else {
return Err("Steam not initialized".to_string());
};
let user_stats = client.user_stats();
user_stats
.reset_all_stats(achievements_too)
.map_err(|_| "Failed to reset stats".to_string())?;
Ok(())
}
pub fn refresh_friends(&mut self) {
let Some(client) = &self.client else {
return;
};
let friends_interface = client.friends();
let friend_list = friends_interface.get_friends(FriendFlags::IMMEDIATE);
self.friends.clear();
for friend in friend_list {
let name = friend.name();
let state = friend.state();
self.friends.push(FriendInfo {
name,
steam_id: friend.id(),
state: state.into(),
});
}
self.friends.sort_by(|a, b| {
let a_online = a.state.is_online();
let b_online = b.state.is_online();
match (a_online, b_online) {
(true, false) => std::cmp::Ordering::Less,
(false, true) => std::cmp::Ordering::Greater,
_ => a.name.to_lowercase().cmp(&b.name.to_lowercase()),
}
});
}
pub fn is_initialized(&self) -> bool {
self.initialized
}
pub fn client(&self) -> Option<&Client> {
self.client.as_ref()
}
pub fn setup_networking_callbacks(&mut self) {
if self.networking_callbacks_registered {
return;
}
let Some(client) = &self.client else {
return;
};
let networking = client.networking_messages();
let pending_requests = Arc::clone(&self.pending_session_requests);
networking.session_request_callback(move |request| {
let remote_identity = request.remote();
if let Some(steam_id) = remote_identity.steam_id() {
if let Ok(mut requests) = pending_requests.lock()
&& !requests.iter().any(|r| r.remote_id == steam_id)
{
requests.push(PendingSessionRequest {
remote_id: steam_id,
remote_name: format!("{:?}", steam_id),
});
}
request.accept();
}
});
networking.session_failed_callback(|info| {
if let Some(identity) = info.identity_remote()
&& let Some(steam_id) = identity.steam_id()
{
tracing::warn!("Session failed with peer: {:?}", steam_id);
}
});
self.networking_callbacks_registered = true;
tracing::info!("Steam networking callbacks registered");
}
pub fn send_message(
&mut self,
target: SteamId,
data: &[u8],
channel: u32,
reliable: bool,
) -> Result<(), String> {
let Some(client) = &self.client else {
return Err("Steam not initialized".to_string());
};
let networking = client.networking_messages();
let identity = NetworkingIdentity::new_steam_id(target);
let flags = if reliable {
SendFlags::RELIABLE_NO_NAGLE | SendFlags::AUTO_RESTART_BROKEN_SESSION
} else {
SendFlags::UNRELIABLE_NO_NAGLE
};
networking
.send_message_to_user(identity, flags, data, channel)
.map_err(|error| format!("Failed to send message: {error:?}"))?;
let target_name = self
.friends
.iter()
.find(|f| f.steam_id == target)
.map(|f| f.name.clone())
.unwrap_or_else(|| format!("{:?}", target));
self.received_messages.push(NetworkMessage {
sender_name: self.user_name.clone(),
sender_id: self.user_id.unwrap_or(target),
data: data.to_vec(),
channel,
is_outgoing: true,
});
if !self.active_sessions.iter().any(|s| s.steam_id == target) {
self.active_sessions.push(ActiveSession {
steam_id: target,
name: target_name,
state: SessionState::Connecting,
});
}
Ok(())
}
pub fn receive_messages(&mut self, channel: u32, max_messages: usize) {
let Some(client) = &self.client else {
return;
};
let networking = client.networking_messages();
let messages = networking.receive_messages_on_channel(channel, max_messages);
for message in messages {
let identity = message.identity_peer();
if let Some(steam_id) = identity.steam_id() {
let sender_name = self
.friends
.iter()
.find(|f| f.steam_id == steam_id)
.map(|f| f.name.clone())
.unwrap_or_else(|| format!("{:?}", steam_id));
self.received_messages.push(NetworkMessage {
sender_name,
sender_id: steam_id,
data: message.data().to_vec(),
channel: message.channel() as u32,
is_outgoing: false,
});
if let Some(session) = self
.active_sessions
.iter_mut()
.find(|s| s.steam_id == steam_id)
{
session.state = SessionState::Connected;
} else {
let name = self
.friends
.iter()
.find(|f| f.steam_id == steam_id)
.map(|f| f.name.clone())
.unwrap_or_else(|| format!("{:?}", steam_id));
self.active_sessions.push(ActiveSession {
steam_id,
name,
state: SessionState::Connected,
});
}
}
}
}
pub fn process_pending_requests(&mut self) {
if let Ok(mut requests) = self.pending_session_requests.lock() {
for request in requests.drain(..) {
if !self
.active_sessions
.iter()
.any(|s| s.steam_id == request.remote_id)
{
self.active_sessions.push(ActiveSession {
steam_id: request.remote_id,
name: request.remote_name,
state: SessionState::Connected,
});
}
}
}
}
pub fn close_session(&mut self, steam_id: SteamId) {
if self.client.is_some() {
unsafe {
let net = steamworks_sys::SteamAPI_SteamNetworkingMessages_SteamAPI_v002();
if !net.is_null() {
let mut identity: steamworks_sys::SteamNetworkingIdentity = std::mem::zeroed();
steamworks_sys::SteamAPI_SteamNetworkingIdentity_SetSteamID64(
&mut identity,
steam_id.raw(),
);
steamworks_sys::SteamAPI_ISteamNetworkingMessages_CloseSessionWithUser(
net, &identity,
);
}
}
}
self.active_sessions.retain(|s| s.steam_id != steam_id);
}
pub fn close_all_sessions(&mut self) {
if self.client.is_some() {
unsafe {
let net = steamworks_sys::SteamAPI_SteamNetworkingMessages_SteamAPI_v002();
if !net.is_null() {
for session in &self.active_sessions {
let mut identity: steamworks_sys::SteamNetworkingIdentity =
std::mem::zeroed();
steamworks_sys::SteamAPI_SteamNetworkingIdentity_SetSteamID64(
&mut identity,
session.steam_id.raw(),
);
steamworks_sys::SteamAPI_ISteamNetworkingMessages_CloseSessionWithUser(
net, &identity,
);
}
}
}
}
self.active_sessions.clear();
}
pub fn clear_messages(&mut self) {
self.received_messages.clear();
}
pub fn get_session_state(&self, steam_id: SteamId) -> SessionState {
let Some(client) = &self.client else {
return SessionState::None;
};
let networking = client.networking_messages();
let identity = NetworkingIdentity::new_steam_id(steam_id);
let (state, _, _) = networking.get_session_connection_info(&identity);
match state {
steamworks::networking_types::NetworkingConnectionState::None => SessionState::None,
steamworks::networking_types::NetworkingConnectionState::Connecting => {
SessionState::Connecting
}
steamworks::networking_types::NetworkingConnectionState::FindingRoute => {
SessionState::Connecting
}
steamworks::networking_types::NetworkingConnectionState::Connected => {
SessionState::Connected
}
steamworks::networking_types::NetworkingConnectionState::ClosedByPeer => {
SessionState::ClosedByPeer
}
steamworks::networking_types::NetworkingConnectionState::ProblemDetectedLocally => {
SessionState::ProblemDetected
}
}
}
pub fn refresh_session_states(&mut self) {
let Some(client) = &self.client else {
return;
};
let networking = client.networking_messages();
for session in &mut self.active_sessions {
let identity = NetworkingIdentity::new_steam_id(session.steam_id);
let (state, _, _) = networking.get_session_connection_info(&identity);
session.state = match state {
steamworks::networking_types::NetworkingConnectionState::None => SessionState::None,
steamworks::networking_types::NetworkingConnectionState::Connecting => {
SessionState::Connecting
}
steamworks::networking_types::NetworkingConnectionState::FindingRoute => {
SessionState::Connecting
}
steamworks::networking_types::NetworkingConnectionState::Connected => {
SessionState::Connected
}
steamworks::networking_types::NetworkingConnectionState::ClosedByPeer => {
SessionState::ClosedByPeer
}
steamworks::networking_types::NetworkingConnectionState::ProblemDetectedLocally => {
SessionState::ProblemDetected
}
};
}
self.active_sessions
.retain(|s| !matches!(s.state, SessionState::None | SessionState::ClosedByPeer));
}
pub fn networking_messages(&self) -> Option<NetworkingMessages> {
self.client.as_ref().map(|c| c.networking_messages())
}
pub fn open_invite_dialog(&self, connect_string: &str) {
let Some(client) = &self.client else {
return;
};
client
.friends()
.activate_invite_dialog_connect_string(connect_string);
}
pub fn open_overlay_to_user(&self, dialog: &str, user: SteamId) {
let Some(client) = &self.client else {
return;
};
client.friends().activate_game_overlay_to_user(dialog, user);
}
pub fn set_rich_presence(&self, key: &str, value: &str) {
let Some(client) = &self.client else {
return;
};
client.friends().set_rich_presence(key, Some(value));
}
pub fn clear_rich_presence(&self) {
let Some(client) = &self.client else {
return;
};
client.friends().clear_rich_presence();
}
}