#![doc = include_str!("../README.md")]
#[macro_use]
extern crate thiserror;
#[macro_use]
extern crate bitflags;
use screenshots::Screenshots;
#[cfg(feature = "raw-bindings")]
pub use steamworks_sys as sys;
#[cfg(not(feature = "raw-bindings"))]
use steamworks_sys as sys;
use sys::{EServerMode, ESteamAPIInitResult, SteamErrMsg};
use core::ffi::c_void;
use std::collections::HashMap;
use std::ffi::{c_char, CStr, CString};
use std::fmt::{self, Debug, Formatter};
use std::sync::mpsc::Sender;
use std::sync::{Arc, Mutex, Weak};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
pub use crate::app::*;
pub use crate::callback::*;
pub use crate::error::*;
pub use crate::friends::*;
pub use crate::input::*;
pub use crate::matchmaking::*;
pub use crate::matchmaking_servers::*;
pub use crate::networking::*;
pub use crate::remote_play::*;
pub use crate::remote_storage::*;
pub use crate::server::*;
pub use crate::timeline::*;
pub use crate::ugc::*;
pub use crate::user::*;
pub use crate::user_stats::*;
pub use crate::utils::*;
#[macro_use]
mod callback;
mod app;
mod error;
mod friends;
mod input;
mod matchmaking;
mod matchmaking_servers;
mod networking;
pub mod networking_messages;
pub mod networking_sockets;
mod networking_sockets_callback;
pub mod networking_types;
pub mod networking_utils;
mod remote_play;
mod remote_storage;
pub mod screenshots;
mod server;
pub mod timeline;
mod ugc;
mod user;
mod user_stats;
mod utils;
pub type SResult<T> = Result<T, SteamError>;
pub type SIResult<T> = Result<T, SteamAPIInitError>;
pub(crate) fn to_steam_result(result: sys::EResult) -> SResult<()> {
if result == sys::EResult::k_EResultOK {
Ok(())
} else {
Err(result.into())
}
}
pub struct Client {
inner: Arc<Inner>,
}
impl Clone for Client {
fn clone(&self) -> Self {
Client {
inner: self.inner.clone(),
}
}
}
struct Inner {
manager: Manager,
callbacks: Callbacks,
networking_sockets_data: Mutex<NetworkingSocketsData>,
}
struct Callbacks {
callbacks: Mutex<HashMap<i32, Box<dyn FnMut(*mut c_void) + Send + 'static>>>,
call_results:
Mutex<HashMap<sys::SteamAPICall_t, Box<dyn FnOnce(*mut c_void, bool) + Send + 'static>>>,
}
impl Inner {
pub fn run_callbacks(&self) {
self.run_callbacks_raw(|cb_discrim, data| {
let mut callbacks = self.callbacks.callbacks.lock().unwrap();
if let Some(cb) = callbacks.get_mut(&cb_discrim) {
cb(data);
}
});
}
pub fn process_callbacks(&self, mut callback_handler: impl FnMut(CallbackResult)) {
self.run_callbacks_raw(|cb_discrim, data| {
{
let mut callbacks = self.callbacks.callbacks.lock().unwrap();
if let Some(cb) = callbacks.get_mut(&cb_discrim) {
cb(data);
}
}
let cb_result = unsafe { CallbackResult::from_raw(cb_discrim, data) };
if let Some(cb_result) = cb_result {
callback_handler(cb_result);
}
});
}
fn run_callbacks_raw(&self, mut callback_handler: impl FnMut(i32, *mut c_void)) {
unsafe {
let pipe = self.manager.get_pipe();
sys::SteamAPI_ManualDispatch_RunFrame(pipe);
let mut callback = std::mem::zeroed();
let mut apicall_result = Vec::new();
while sys::SteamAPI_ManualDispatch_GetNextCallback(pipe, &mut callback) {
if callback.m_iCallback == sys::SteamAPICallCompleted_t_k_iCallback as i32 {
let apicall = callback
.m_pubParam
.cast::<sys::SteamAPICallCompleted_t>()
.read_unaligned();
apicall_result.resize(apicall.m_cubParam as usize, 0u8);
let mut failed = false;
if sys::SteamAPI_ManualDispatch_GetAPICallResult(
pipe,
apicall.m_hAsyncCall,
apicall_result.as_mut_ptr().cast(),
apicall.m_cubParam as _,
apicall.m_iCallback,
&mut failed,
) {
let mut call_results = self.callbacks.call_results.lock().unwrap();
if let Some(cb) = call_results.remove(&{ apicall.m_hAsyncCall }) {
cb(apicall_result.as_mut_ptr().cast(), failed);
}
}
} else {
callback_handler(callback.m_iCallback, callback.m_pubParam.cast());
}
sys::SteamAPI_ManualDispatch_FreeLastCallback(pipe);
}
}
}
}
struct NetworkingSocketsData {
sockets: HashMap<
sys::HSteamListenSocket,
(
Weak<networking_sockets::InnerSocket>,
Sender<networking_types::ListenSocketEvent>,
),
>,
independent_connections:
HashMap<sys::HSteamNetConnection, Sender<networking_types::NetConnectionEvent>>,
connection_callback: Weak<CallbackHandle>,
}
pub fn restart_app_if_necessary(app_id: AppId) -> bool {
unsafe { sys::SteamAPI_RestartAppIfNecessary(app_id.0) }
}
fn static_assert_send<T: Send>() {}
fn static_assert_sync<T>()
where
T: Sync,
{
}
impl Client {
unsafe fn steam_api_init_flat(p_out_err_msg: *mut SteamErrMsg) -> ESteamAPIInitResult {
unsafe { sys::SteamAPI_InitFlat(p_out_err_msg) }
}
pub fn init() -> SIResult<Client> {
static_assert_send::<Client>();
static_assert_sync::<Client>();
unsafe {
let mut err_msg: sys::SteamErrMsg = [0; 1024];
let result = Self::steam_api_init_flat(&mut err_msg);
if result != sys::ESteamAPIInitResult::k_ESteamAPIInitResult_OK {
return Err(SteamAPIInitError::from_result_and_message(result, err_msg));
}
sys::SteamAPI_ManualDispatch_Init();
let client = Arc::new(Inner {
manager: Manager::Client,
callbacks: Callbacks {
callbacks: Mutex::new(HashMap::new()),
call_results: Mutex::new(HashMap::new()),
},
networking_sockets_data: Mutex::new(NetworkingSocketsData {
sockets: Default::default(),
independent_connections: Default::default(),
connection_callback: Default::default(),
}),
});
Ok(Client { inner: client })
}
}
pub fn init_app<ID: Into<AppId>>(app_id: ID) -> SIResult<Client> {
let app_id = app_id.into().0.to_string();
std::env::set_var("SteamAppId", &app_id);
std::env::set_var("SteamGameId", app_id);
Client::init()
}
}
impl Client {
pub fn run_callbacks(&self) {
self.inner.run_callbacks()
}
pub fn process_callbacks(&self, mut callback_handler: impl FnMut(CallbackResult)) {
self.inner.process_callbacks(&mut callback_handler)
}
pub fn register_callback<C, F>(&self, f: F) -> CallbackHandle
where
C: Callback,
F: FnMut(C) + 'static + Send,
{
unsafe { register_callback(&self.inner, f) }
}
pub fn utils(&self) -> Utils {
unsafe {
let utils = sys::SteamAPI_SteamUtils_v010();
debug_assert!(!utils.is_null());
Utils {
utils: utils,
_inner: self.inner.clone(),
}
}
}
pub fn matchmaking(&self) -> Matchmaking {
unsafe {
let mm = sys::SteamAPI_SteamMatchmaking_v009();
debug_assert!(!mm.is_null());
Matchmaking {
mm: mm,
inner: self.inner.clone(),
}
}
}
pub fn matchmaking_servers(&self) -> MatchmakingServers {
unsafe {
let mm = sys::SteamAPI_SteamMatchmakingServers_v002();
debug_assert!(!mm.is_null());
MatchmakingServers {
mms: mm,
_inner: self.inner.clone(),
}
}
}
pub fn networking(&self) -> Networking {
unsafe {
let net = sys::SteamAPI_SteamNetworking_v006();
debug_assert!(!net.is_null());
Networking {
net: net,
_inner: self.inner.clone(),
}
}
}
pub fn apps(&self) -> Apps {
unsafe {
let apps = sys::SteamAPI_SteamApps_v009();
debug_assert!(!apps.is_null());
Apps {
apps: apps,
_inner: self.inner.clone(),
}
}
}
pub fn friends(&self) -> Friends {
unsafe {
let friends = sys::SteamAPI_SteamFriends_v018();
debug_assert!(!friends.is_null());
Friends {
friends: friends,
inner: self.inner.clone(),
}
}
}
pub fn input(&self) -> Input {
unsafe {
let input = sys::SteamAPI_SteamInput_v006();
debug_assert!(!input.is_null());
Input {
input,
_inner: self.inner.clone(),
}
}
}
pub fn user(&self) -> User {
unsafe {
let user = sys::SteamAPI_SteamUser_v023();
debug_assert!(!user.is_null());
User {
user,
_inner: self.inner.clone(),
}
}
}
pub fn user_stats(&self) -> UserStats {
unsafe {
let us = sys::SteamAPI_SteamUserStats_v013();
debug_assert!(!us.is_null());
UserStats {
user_stats: us,
inner: self.inner.clone(),
}
}
}
pub fn remote_play(&self) -> RemotePlay {
unsafe {
let rp = sys::SteamAPI_SteamRemotePlay_v004();
debug_assert!(!rp.is_null());
RemotePlay {
rp,
inner: self.inner.clone(),
}
}
}
pub fn remote_storage(&self) -> RemoteStorage {
unsafe {
let rs = sys::SteamAPI_SteamRemoteStorage_v016();
debug_assert!(!rs.is_null());
let util = sys::SteamAPI_SteamUtils_v010();
debug_assert!(!util.is_null());
RemoteStorage {
rs,
util,
inner: self.inner.clone(),
}
}
}
pub fn screenshots(&self) -> Screenshots {
unsafe {
let screenshots = sys::SteamAPI_SteamScreenshots_v003();
debug_assert!(!screenshots.is_null());
Screenshots {
screenshots,
_inner: self.inner.clone(),
}
}
}
pub fn ugc(&self) -> UGC {
unsafe {
let ugc = sys::SteamAPI_SteamUGC_v021();
debug_assert!(!ugc.is_null());
UGC {
ugc,
inner: self.inner.clone(),
}
}
}
pub fn timeline(&self) -> Timeline {
unsafe {
let timeline = sys::SteamAPI_SteamTimeline_v004();
Timeline {
timeline,
disabled: timeline.is_null(),
_inner: self.inner.clone(),
}
}
}
pub fn networking_messages(&self) -> networking_messages::NetworkingMessages {
unsafe {
let net = sys::SteamAPI_SteamNetworkingMessages_SteamAPI_v002();
debug_assert!(!net.is_null());
networking_messages::NetworkingMessages {
net,
inner: self.inner.clone(),
}
}
}
pub fn networking_sockets(&self) -> networking_sockets::NetworkingSockets {
unsafe {
let sockets = sys::SteamAPI_SteamNetworkingSockets_SteamAPI_v012();
debug_assert!(!sockets.is_null());
networking_sockets::NetworkingSockets {
sockets,
inner: self.inner.clone(),
}
}
}
pub fn networking_utils(&self) -> networking_utils::NetworkingUtils {
unsafe {
let utils = sys::SteamAPI_SteamNetworkingUtils_SteamAPI_v004();
debug_assert!(!utils.is_null());
networking_utils::NetworkingUtils {
utils,
inner: self.inner.clone(),
}
}
}
}
enum Manager {
Client,
Server,
}
impl Manager {
fn get_pipe(&self) -> sys::HSteamPipe {
match self {
Manager::Client => unsafe { sys::SteamAPI_GetHSteamPipe() },
Manager::Server => unsafe { sys::SteamGameServer_GetHSteamPipe() },
}
}
}
impl Drop for Manager {
fn drop(&mut self) {
match self {
Manager::Client => unsafe { sys::SteamAPI_Shutdown() },
Manager::Server => unsafe { sys::SteamGameServer_Shutdown() },
}
}
}
#[derive(Clone, Copy, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct SteamId(pub(crate) u64);
impl SteamId {
pub fn from_raw(id: u64) -> SteamId {
SteamId(id)
}
pub fn raw(&self) -> u64 {
self.0
}
pub fn is_invalid(&self) -> bool {
unsafe {
let bits = sys::CSteamID_SteamID_t {
m_unAll64Bits: self.0,
};
bits.m_comp.m_EAccountType()
== sys::EAccountType::k_EAccountTypeInvalid as std::os::raw::c_uint
}
}
pub fn account_id(&self) -> AccountId {
unsafe {
let bits = sys::CSteamID_SteamID_t {
m_unAll64Bits: self.0,
};
AccountId(bits.m_comp.m_unAccountID())
}
}
pub fn universe(&self) -> Universe {
let bits = sys::CSteamID_SteamID_t {
m_unAll64Bits: self.0,
};
match unsafe { bits.m_comp }.m_EUniverse() {
sys::EUniverse::k_EUniversePublic => Universe::Public,
sys::EUniverse::k_EUniverseBeta => Universe::Beta,
sys::EUniverse::k_EUniverseInternal => Universe::Internal,
sys::EUniverse::k_EUniverseDev => Universe::Dev,
_ => Universe::Invalid,
}
}
pub fn account_type(&self) -> AccountType {
let bits = sys::CSteamID_SteamID_t {
m_unAll64Bits: self.0,
};
match unsafe { bits.m_comp }.m_EAccountType() {
x if x == sys::EAccountType::k_EAccountTypeIndividual as u32 => AccountType::Individual,
x if x == sys::EAccountType::k_EAccountTypeMultiseat as u32 => AccountType::Multiseat,
x if x == sys::EAccountType::k_EAccountTypeGameServer as u32 => AccountType::GameServer,
x if x == sys::EAccountType::k_EAccountTypeAnonGameServer as u32 => {
AccountType::AnonGameServer
}
x if x == sys::EAccountType::k_EAccountTypePending as u32 => AccountType::Pending,
x if x == sys::EAccountType::k_EAccountTypeContentServer as u32 => {
AccountType::ContentServer
}
x if x == sys::EAccountType::k_EAccountTypeClan as u32 => AccountType::Clan,
x if x == sys::EAccountType::k_EAccountTypeChat as u32 => AccountType::Chat,
x if x == sys::EAccountType::k_EAccountTypeConsoleUser as u32 => {
AccountType::ConsoleUser
}
x if x == sys::EAccountType::k_EAccountTypeAnonUser as u32 => AccountType::AnonUser,
_ => AccountType::Invalid,
}
}
pub fn steamid32(&self) -> String {
let account_id = self.account_id().raw();
let last_bit = account_id & 1;
format!("STEAM_0:{}:{}", last_bit, (account_id >> 1))
}
}
#[derive(Clone, Copy, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[repr(u32)]
pub enum Universe {
Invalid = 0,
Public = 1,
Beta = 2,
Internal = 3,
Dev = 4,
}
#[derive(Clone, Copy, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[repr(u32)]
pub enum AccountType {
Invalid = 0,
Individual = 1,
Multiseat = 2,
GameServer = 3,
AnonGameServer = 4,
Pending = 5,
ContentServer = 6,
Clan = 7,
Chat = 8,
ConsoleUser = 9,
AnonUser = 10,
}
#[derive(Clone, Copy, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct AccountId(pub(crate) u32);
impl AccountId {
pub fn from_raw(id: u32) -> AccountId {
AccountId(id)
}
pub fn raw(&self) -> u32 {
self.0
}
}
#[derive(Clone, Copy, Debug, Ord, PartialOrd, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct GameId(#[cfg_attr(feature = "serde", serde(rename = "game_id"))] pub(crate) u64);
impl GameId {
pub fn from_raw(id: u64) -> GameId {
GameId(id)
}
pub fn raw(&self) -> u64 {
self.0
}
pub fn app_id(&self) -> AppId {
AppId((self.0 & 0xFF_FF_FF) as u32)
}
}
#[cfg(test)]
mod tests {
use serial_test::serial;
use super::*;
#[test]
#[serial]
fn basic_test() {
let client = Client::init().unwrap();
let _cb = client.register_callback(|p: PersonaStateChange| {
println!("Got callback: {:?}", p);
});
let utils = client.utils();
println!("Utils:");
println!("AppId: {:?}", utils.app_id());
println!("UI Language: {}", utils.ui_language());
let apps = client.apps();
println!("Apps");
println!("IsInstalled(480): {}", apps.is_app_installed(AppId(480)));
println!("InstallDir(480): {}", apps.app_install_dir(AppId(480)));
println!("BuildId: {}", apps.app_build_id());
println!("AppOwner: {:?}", apps.app_owner());
println!("Langs: {:?}", apps.available_game_languages());
println!("Lang: {}", apps.current_game_language());
println!("Beta: {:?}", apps.current_beta_name());
let friends = client.friends();
println!("Friends");
let list = friends.get_friends(FriendFlags::IMMEDIATE);
println!("{:?}", list);
for f in &list {
println!("Friend: {:?} - {}({:?})", f.id(), f.name(), f.state());
friends.request_user_information(f.id(), true);
}
friends.request_user_information(SteamId(76561198174976054), true);
for _ in 0..50 {
client.run_callbacks();
::std::thread::sleep(::std::time::Duration::from_millis(100));
}
}
#[test]
fn steamid_test() {
let steamid = SteamId(76561198040894045);
assert_eq!("STEAM_0:1:40314158", steamid.steamid32());
let steamid = SteamId(76561198174976054);
assert_eq!("STEAM_0:0:107355163", steamid.steamid32());
}
}