From c7cc265b776e08bc23631aeb8be631c9cac4c13a Mon Sep 17 00:00:00 2001 From: Brian Bowman Date: Mon, 7 Sep 2020 03:27:28 -0500 Subject: [PATCH] [WIP] Switch to manual dispatch API Also, - Update deps - Add debug hook - Tweak build script - Add logging through the tracing crate --- Cargo.toml | 9 +- README.md | 51 ++++++- src/callbacks.rs | 123 ++++++++++++---- src/lib.rs | 274 +++++++++++++++++++++++------------- src/steam/common.rs | 6 +- src/steam/mod.rs | 2 +- src/steam/remote_storage.rs | 24 ++-- src/steam/ugc.rs | 36 ++--- src/steam/user_stats.rs | 91 ++++++------ steamworks-sys/Cargo.toml | 1 - steamworks-sys/build.rs | 37 +++-- steamworks-sys/src/lib.cpp | 30 ---- steamworks-sys/wrapper.hpp | 21 --- 13 files changed, 413 insertions(+), 292 deletions(-) delete mode 100644 steamworks-sys/src/lib.cpp diff --git a/Cargo.toml b/Cargo.toml index b230c5a90..c854aa26a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,12 +12,15 @@ bitflags = "1" chrono = "0.4" derive_more = "0.99" enum-primitive-derive = "0.2" +fnv = "1" genawaiter = { version = "0.99", features = ["futures03"] } -futures = "0.3" +futures = { version = "0.3", default-features = false, features = ["std", "async-await"] } +futures-intrusive = "0.3" num-traits = "0.2" once_cell = "1" -parking_lot = "0.10" +parking_lot = "0.11" slotmap = "0.4" -smol = "0.1" snafu = "0.6" +static_assertions = "1" steamworks-sys = { path = "./steamworks-sys" } +tracing = "0.1" diff --git a/README.md b/README.md index 806368901..f5d03b966 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,55 @@ # Steamworks -Futures-enabled bindings to a tiny portion of the Steamworks API. +Async, cross-platform, Rust bindings for the [Steamworks API](https://partner.steamgames.com/doc/sdk/api). -### [Docs](https://seeker14491.github.io/steamworks-rs/steamworks) +Only a (very) tiny portion of the Steamworks API has been implemented in this library — only the functionality I use. The API is unstable and subject to change at any time. -## Requirements +The bindings aim to be easy to use and idiomatic, while still following the structure of the official C++ API close enough so the official Steamworks API docs remain helpful. -- Clang (to run bindgen) +### [Docs](https://seeker14491.github.io/steamworks-rs/steamworks) *(for the latest tagged release)* -Additionally, to run your binary that depends on this library, you will need to include the necessary `.dll`, `.dylib`, `.so` (depending on the platform) next to the executable. These are found in the `steamworks-sys\steamworks_sdk\redistributable_bin` directory. Note that this isn't necessary if you're running the executable through `cargo run`. Either way, you will probably need a `steam_appid.txt` file, as described in the [official docs](https://partner.steamgames.com/doc/sdk/api#SteamAPI_Init). +## Example -Also, add the following to your crate's `.cargo/config` file to configure your compiled binary, on Unix platforms, to locate the Steamworks shared library next to the executable: +The following is a complete example showing basic use of the library. We get a handle to a leaderboard using the leaderboard's name, then we download the top 5 leaderboard entries, and then for each entry we resolve the player's name and print it along with the player's time: + +```rust +fn main() -> Result<(), anyhow::Error> { + let client = steamworks::Client::init()?; + + futures::executor::block_on(async { + let leaderboard_handle = client.find_leaderboard("Broken Symmetry_1_stable").await?; + let top_5_entries = leaderboard_handle.download_global(1, 5, 0).await; + for entry in &top_5_entries { + let player_name = entry.steam_id.persona_name(&client).await; + println!("player, time (ms): {}, {}", &player_name, entry.score); + } + + Ok(()) + }) +} +``` + +Run under the context of [Distance](http://survivethedistance.com/), this code produced this output when I ran it: + +``` +player, time (ms): Brionac, 74670 +player, time (ms): Tiedye, 74990 +player, time (ms): Seekr, 75160 +player, time (ms): Don Quixote, 75630 +player, time (ms): -DarkAngel-, 75640 +``` + +In this example we used `block_on()` from the [`futures`](https://crates.io/crates/futures) crate, but this library is async executor agnostic; you can use any other executor you like. `anyhow::Error` from the [`anyhow`](https://crates.io/crates/anyhow) crate was used as the error type for easy error handling. + +## Extra build requirements + +You'll need Clang installed, as this crate runs `bindgen` at build time. See [here](https://rust-lang.github.io/rust-bindgen/requirements.html) for more info. As for the Steamworks SDK, it's included in this repo; there's no need to download it separately. + +## A note on distributing binaries that depend on this library + +To run your binary that depends on this library, you will need to include the necessary `.dll`, `.dylib`, `.so` (depending on the platform) next to the executable. These are found in the `steamworks-sys\steamworks_sdk\redistributable_bin` directory. Note that this isn't necessary if you're running the executable through `cargo run`. Either way, you will probably need a `steam_appid.txt` file, as described in the [official docs](https://partner.steamgames.com/doc/sdk/api#SteamAPI_Init). + +Also, add the following to your crate's `.cargo/config.toml` file (make it if it doesn't exist) to configure your compiled binary, on Linux, to locate the Steamworks shared library next to the executable: ``` [target.'cfg(unix)'] diff --git a/src/callbacks.rs b/src/callbacks.rs index 626f365f6..1dc70f88f 100644 --- a/src/callbacks.rs +++ b/src/callbacks.rs @@ -1,12 +1,79 @@ use crate::steam::SteamId; +use az::WrappingCast; use bitflags::bitflags; -use once_cell::sync::Lazy; +use futures::Stream; use parking_lot::Mutex; use slotmap::DenseSlotMap; +use std::{convert::TryFrom, mem}; use steamworks_sys as sys; pub(crate) type CallbackStorage = - Lazy>>>; + Mutex>>; + +pub(crate) fn register_to_receive_callback( + dispatcher: &impl CallbackDispatcher, +) -> impl Stream + Send { + let (tx, rx) = futures::channel::mpsc::unbounded(); + dispatcher.storage().lock().insert(tx); + rx +} + +pub(crate) unsafe fn dispatch_callbacks( + callback_dispatchers: &CallbackDispatchers, + callback_msg: sys::CallbackMsg_t, +) { + match callback_msg.m_iCallback.wrapping_cast() { + sys::PersonaStateChange_t_k_iCallback => callback_dispatchers + .persona_state_change + .dispatch(callback_msg.m_pubParam, callback_msg.m_cubParam), + sys::SteamShutdown_t_k_iCallback => callback_dispatchers + .steam_shutdown + .dispatch(callback_msg.m_pubParam, callback_msg.m_cubParam), + _ => {} + } +} + +#[derive(Debug, Default)] +pub(crate) struct CallbackDispatchers { + pub(crate) persona_state_change: PersonaStateChangeDispatcher, + pub(crate) steam_shutdown: SteamShutdownDispatcher, +} + +impl CallbackDispatchers { + pub(crate) fn new() -> Self { + Self::default() + } +} + +pub(crate) trait CallbackDispatcher { + type RawCallbackData; + type MappedCallbackData: Clone + Send + 'static; + + fn storage(&self) -> &CallbackStorage; + fn map_callback_data(raw: &Self::RawCallbackData) -> Self::MappedCallbackData; + + unsafe fn dispatch(&self, callback_data: *const u8, callback_data_len: i32) { + assert!(!callback_data.is_null()); + assert_eq!( + callback_data.align_offset(mem::align_of::()), + 0 + ); + assert_eq!( + usize::try_from(callback_data_len).unwrap(), + mem::size_of::() + ); + + let raw = &*(callback_data as *const Self::RawCallbackData); + let mapped = Self::map_callback_data(raw); + + let mut storage = self.storage().lock(); + storage.retain(|_key, tx| match tx.unbounded_send(mapped.clone()) { + Err(e) if e.is_disconnected() => false, + Err(e) => panic!(e), + Ok(()) => true, + }); + } +} /// #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)] @@ -36,40 +103,36 @@ bitflags! { } } -pub(crate) static PERSONA_STATE_CHANGED: CallbackStorage = - Lazy::new(|| Mutex::new(DenseSlotMap::new())); +// TODO: macro to write these implementations +#[derive(Debug, Default)] +pub(crate) struct PersonaStateChangeDispatcher(CallbackStorage); + +impl CallbackDispatcher for PersonaStateChangeDispatcher { + type RawCallbackData = sys::PersonaStateChange_t; + type MappedCallbackData = PersonaStateChange; -pub(crate) unsafe extern "C" fn on_persona_state_changed(params: *mut sys::PersonaStateChange_t) { - let params = *params; - let params = PersonaStateChange { - steam_id: params.m_ulSteamID.into(), - change_flags: PersonaStateChangeFlags::from_bits_truncate(params.m_nChangeFlags as u32), - }; + fn storage(&self) -> &CallbackStorage { + &self.0 + } - forward_callback(&PERSONA_STATE_CHANGED, params); + fn map_callback_data(raw: &sys::PersonaStateChange_t) -> PersonaStateChange { + PersonaStateChange { + steam_id: raw.m_ulSteamID.into(), + change_flags: PersonaStateChangeFlags::from_bits_truncate(raw.m_nChangeFlags as u32), + } + } } -pub(crate) static STEAM_SHUTDOWN: CallbackStorage<()> = - Lazy::new(|| Mutex::new(DenseSlotMap::new())); +#[derive(Debug, Default)] +pub(crate) struct SteamShutdownDispatcher(CallbackStorage<()>); -pub(crate) unsafe extern "C" fn on_steam_shutdown(_: *mut sys::SteamShutdown_t) { - forward_callback(&STEAM_SHUTDOWN, ()); -} +impl CallbackDispatcher for SteamShutdownDispatcher { + type RawCallbackData = sys::SteamShutdown_t; + type MappedCallbackData = (); -fn forward_callback(storage: &CallbackStorage, params: T) { - let mut keys_to_remove = Vec::new(); - let mut map = storage.lock(); - for (k, tx) in map.iter() { - if let Err(e) = tx.unbounded_send(params) { - if e.is_disconnected() { - keys_to_remove.push(k); - } else { - panic!(e); - } - } + fn storage(&self) -> &CallbackStorage<()> { + &self.0 } - for k in &keys_to_remove { - map.remove(*k); - } + fn map_callback_data(_raw: &sys::SteamShutdown_t) {} } diff --git a/src/lib.rs b/src/lib.rs index 6c7e69635..6704cb579 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,6 +27,7 @@ deprecated_in_future, macro_use_extern_crate, missing_debug_implementations, + unreachable_pub, unused_qualifications )] #![allow(dead_code)] @@ -36,26 +37,31 @@ pub mod callbacks; mod steam; mod string_ext; +pub use error::InitError; pub use steam::*; -use crate::callbacks::CallbackStorage; +use crate::callbacks::CallbackDispatchers; use az::WrappingCast; +use derive_more::Deref; +use fnv::FnvHashMap; use futures::{future::BoxFuture, FutureExt, Stream}; -use smol::Timer; -use snafu::{ensure, Snafu}; +use parking_lot::Mutex; +use snafu::ensure; +use static_assertions::assert_impl_all; use std::{ convert::TryInto, - ffi::c_void, + ffi::{c_void, CStr}, mem::{self, MaybeUninit}, + os::raw::c_char, sync::{ atomic::{self, AtomicBool}, - mpsc::{self, SyncSender}, Arc, }, thread, time::Duration, }; use steamworks_sys as sys; +use tracing::{event, Level}; static STEAM_API_INITIALIZED: AtomicBool = AtomicBool::new(false); @@ -65,20 +71,27 @@ static STEAM_API_INITIALIZED: AtomicBool = AtomicBool::new(false); #[derive(Debug, Clone)] pub struct Client(Arc); +assert_impl_all!(Client: Send, Sync); + #[derive(Debug)] struct ClientInner { - thread_shutdown: SyncSender<()>, - callback_manager: *mut sys::CallbackManager, - friends: *mut sys::ISteamFriends, - remote_storage: *mut sys::ISteamRemoteStorage, - ugc: *mut sys::ISteamUGC, - user: *mut sys::ISteamUser, - user_stats: *mut sys::ISteamUserStats, - utils: *mut sys::ISteamUtils, + thread_shutdown: std::sync::mpsc::SyncSender<()>, + callback_dispatchers: CallbackDispatchers, + call_result_handles: + Mutex>>>, + friends: SteamworksInterface, + remote_storage: SteamworksInterface, + ugc: SteamworksInterface, + user: SteamworksInterface, + user_stats: SteamworksInterface, + utils: SteamworksInterface, } -unsafe impl Send for ClientInner {} -unsafe impl Sync for ClientInner {} +#[derive(Debug, Copy, Clone, Deref)] +struct SteamworksInterface(*mut T); + +unsafe impl Send for SteamworksInterface {} +unsafe impl Sync for SteamworksInterface {} impl Client { /// Initializes the Steamworks API, yielding a `Client`. @@ -88,49 +101,43 @@ impl Client { pub fn init() -> Result { ensure!( !STEAM_API_INITIALIZED.swap(true, atomic::Ordering::AcqRel), - AlreadyInitialized + error::AlreadyInitialized ); let success = unsafe { sys::SteamAPI_Init() }; if !success { STEAM_API_INITIALIZED.store(false, atomic::Ordering::Release); - return Other.fail(); + return error::Other.fail(); } - let callback_manager = unsafe { - sys::steam_rust_register_callbacks(sys::SteamRustCallbacks { - onPersonaStateChanged: Some(callbacks::on_persona_state_changed), - onSteamShutdown: Some(callbacks::on_steam_shutdown), - }) - }; + unsafe { + sys::SteamAPI_ManualDispatch_Init(); + } - let (shutdown_tx, shutdown_rx) = mpsc::sync_channel(0); - thread::spawn(move || { - smol::run(async { - loop { - unsafe { sys::SteamAPI_RunCallbacks() } + let utils = unsafe { SteamworksInterface(sys::SteamAPI_SteamUtils_v010()) }; + unsafe { + sys::SteamAPI_ISteamUtils_SetWarningMessageHook(*utils, Some(warning_message_hook)); + } - if let Ok(()) = shutdown_rx.try_recv() { - break; - } + let (shutdown_tx, shutdown_rx) = std::sync::mpsc::sync_channel(0); + let client = unsafe { + Client(Arc::new(ClientInner { + thread_shutdown: shutdown_tx, + callback_dispatchers: CallbackDispatchers::new(), + call_result_handles: Mutex::new(FnvHashMap::default()), + friends: SteamworksInterface(sys::SteamAPI_SteamFriends_v017()), + remote_storage: SteamworksInterface(sys::SteamAPI_SteamRemoteStorage_v014()), + ugc: SteamworksInterface(sys::SteamAPI_SteamUGC_v014()), + user: SteamworksInterface(sys::SteamAPI_SteamUser_v021()), + user_stats: SteamworksInterface(sys::SteamAPI_SteamUserStats_v012()), + utils, + })) + }; - Timer::after(Duration::from_millis(1)).await; - } - }); - }); + start_worker_thread(client.clone(), shutdown_rx); + event!(Level::DEBUG, "Steamworks API initialized"); - unsafe { - Ok(Client(Arc::new(ClientInner { - thread_shutdown: shutdown_tx, - callback_manager, - friends: sys::SteamAPI_SteamFriends_v017(), - remote_storage: sys::SteamAPI_SteamRemoteStorage_v014(), - ugc: sys::SteamAPI_SteamUGC_v014(), - user: sys::SteamAPI_SteamUser_v021(), - user_stats: sys::SteamAPI_SteamUserStats_v012(), - utils: sys::SteamAPI_SteamUtils_v010(), - }))) - } + Ok(client) } /// @@ -153,12 +160,12 @@ impl Client { /// pub fn app_id(&self) -> AppId { - unsafe { sys::SteamAPI_ISteamUtils_GetAppID(self.0.utils).into() } + unsafe { sys::SteamAPI_ISteamUtils_GetAppID(*self.0.utils).into() } } /// pub fn steam_id(&self) -> SteamId { - let id = unsafe { sys::SteamAPI_ISteamUser_GetSteamID(self.0.user) }; + let id = unsafe { sys::SteamAPI_ISteamUser_GetSteamID(*self.0.user) }; id.into() } @@ -167,52 +174,29 @@ impl Client { pub fn on_persona_state_changed( &self, ) -> impl Stream + Send { - self.get_callback_stream(&callbacks::PERSONA_STATE_CHANGED) + callbacks::register_to_receive_callback(&self.0.callback_dispatchers.persona_state_change) } /// pub fn on_steam_shutdown(&self) -> impl Stream + Send { - self.get_callback_stream(&callbacks::STEAM_SHUTDOWN) + callbacks::register_to_receive_callback(&self.0.callback_dispatchers.steam_shutdown) } - async fn future_from_call_result_fn( + // TODO: avoid allocating Vec + async unsafe fn register_for_call_result( &self, - magic_number: impl Copy + WrappingCast, - make_call: impl Fn() -> sys::SteamAPICall_t, + handle: sys::SteamAPICall_t, ) -> CallResult { - let mut callback_data: MaybeUninit = MaybeUninit::zeroed(); - let mut failed = true; - while failed { - let api_call = make_call(); - loop { - Timer::after(Duration::from_millis(5)).await; - let completed = unsafe { - sys::SteamAPI_ISteamUtils_GetAPICallResult( - self.0.utils, - api_call, - callback_data.as_mut_ptr() as *mut c_void, - mem::size_of::().try_into().unwrap(), - magic_number.wrapping_cast(), - &mut failed, - ) - }; - - if completed { - break; - } - } - } + let (tx, rx) = futures::channel::oneshot::channel::>(); + self.0.call_result_handles.lock().insert(handle, tx); + rx.map(|x| { + // TODO: handle cancelation + let bytes = x.unwrap(); - unsafe { callback_data.assume_init() } - } - - fn get_callback_stream( - &self, - storage: &CallbackStorage, - ) -> impl Stream + Send { - let (tx, rx) = futures::channel::mpsc::unbounded(); - storage.lock().insert(tx); - rx + assert_eq!(bytes.len(), mem::size_of::()); + *(bytes.as_ptr() as *const CallResult) + }) + .await } } @@ -220,21 +204,121 @@ impl Drop for ClientInner { fn drop(&mut self) { self.thread_shutdown.send(()).unwrap(); unsafe { - sys::steam_rust_unregister_callbacks(self.callback_manager); sys::SteamAPI_Shutdown(); } STEAM_API_INITIALIZED.store(false, atomic::Ordering::Release); + event!(Level::DEBUG, "Steamworks API shut down"); } } -#[derive(Debug, Snafu)] -pub enum InitError { - /// Tried to initialize Steam API when it was already initialized - #[snafu(display("Tried to initialize Steam API when it was already initialized"))] - AlreadyInitialized, +unsafe extern "C" fn warning_message_hook(severity: i32, debug_text: *const c_char) { + let debug_text = CStr::from_ptr(debug_text); + if severity == 1 { + event!(Level::WARN, ?debug_text, "Steam API warning"); + } else { + event!(Level::INFO, ?debug_text, "Steam API message"); + } +} + +fn start_worker_thread(client: Client, shutdown_rx: std::sync::mpsc::Receiver<()>) { + thread::spawn(move || { + unsafe { + let steam_pipe = sys::SteamAPI_GetHSteamPipe(); + loop { + sys::SteamAPI_ManualDispatch_RunFrame(steam_pipe); + let mut callback_msg: MaybeUninit = MaybeUninit::uninit(); + while sys::SteamAPI_ManualDispatch_GetNextCallback( + steam_pipe, + callback_msg.as_mut_ptr(), + ) { + let callback = callback_msg.assume_init(); + + // Check if we're dispatching a call result or a callback + if callback.m_iCallback + == sys::SteamAPICallCompleted_t_k_iCallback.wrapping_cast() + { + // It's a call result + + assert!(!callback.m_pubParam.is_null()); + assert_eq!( + callback + .m_pubParam + .align_offset(mem::align_of::()), + 0 + ); + let call_completed = + &mut *(callback.m_pubParam as *mut sys::SteamAPICallCompleted_t); + + let mut call_result_buf = + vec![0_u8; call_completed.m_cubParam.try_into().unwrap()]; + let mut failed = true; + if sys::SteamAPI_ManualDispatch_GetAPICallResult( + steam_pipe, + call_completed.m_hAsyncCall, + call_result_buf.as_mut_ptr() as *mut c_void, + call_result_buf.len().try_into().unwrap(), + call_completed.m_iCallback, + &mut failed, + ) { + let call_id = call_completed.m_hAsyncCall; + match client.0.call_result_handles.lock().remove(&call_id) { + Some(tx) => { + tx.send(call_result_buf).ok(); + } + None => { + event!( + Level::WARN, + SteamAPICallCompleted_t = ?call_completed, + "a CallResult became available, but its recipient was not found" + ); + } + } + } else { + event!( + Level::ERROR, + SteamAPICallCompleted_t = ?call_completed, + "failed to fetch a completed CallResult" + ); + + // TODO: send error + } + } else { + // It's a callback + + callbacks::dispatch_callbacks(&client.0.callback_dispatchers, callback); + } + + sys::SteamAPI_ManualDispatch_FreeLastCallback(steam_pipe); + } + + if let Ok(()) = shutdown_rx.try_recv() { + event!( + Level::DEBUG, + "worker thread shutting down due to receiving shutdown signal" + ); + + break; + } - /// The Steamworks API failed to initialize (SteamAPI_Init() returned false) - #[snafu(display("The Steamworks API failed to initialize (SteamAPI_Init() returned false)"))] - Other, + thread::sleep(Duration::from_millis(1)); + } + } + }); +} + +mod error { + #[derive(Debug, snafu::Snafu)] + #[snafu(visibility(pub(crate)))] + pub enum InitError { + /// Tried to initialize Steam API when it was already initialized + #[snafu(display("Tried to initialize Steam API when it was already initialized"))] + AlreadyInitialized, + + /// The Steamworks API failed to initialize (SteamAPI_Init() returned false) + #[snafu(display( + "The Steamworks API failed to initialize (SteamAPI_Init() returned false)" + ))] + Other, + } } diff --git a/src/steam/common.rs b/src/steam/common.rs index 0f4e38db9..744ece6a6 100644 --- a/src/steam/common.rs +++ b/src/steam/common.rs @@ -40,10 +40,10 @@ impl SteamId { id.into() } - pub fn persona_name(self, client: &Client) -> impl Future + Send + Sync + '_ { + pub fn persona_name(self, client: &Client) -> impl Future + Send + '_ { let mut persona_state_changes = client.on_persona_state_changed(); let request_in_progress = unsafe { - sys::SteamAPI_ISteamFriends_RequestUserInformation(client.0.friends, self.0, true) + sys::SteamAPI_ISteamFriends_RequestUserInformation(*client.0.friends, self.0, true) }; async move { if request_in_progress { @@ -59,7 +59,7 @@ impl SteamId { unsafe { let name = - sys::SteamAPI_ISteamFriends_GetFriendPersonaName(client.0.friends, self.0); + sys::SteamAPI_ISteamFriends_GetFriendPersonaName(*client.0.friends, self.0); CStr::from_ptr(name) .to_owned() diff --git a/src/steam/mod.rs b/src/steam/mod.rs index 2aa3b01da..b1cf3edec 100644 --- a/src/steam/mod.rs +++ b/src/steam/mod.rs @@ -4,4 +4,4 @@ pub mod user_stats; mod common; -pub use common::*; +pub(crate) use common::*; diff --git a/src/steam/remote_storage.rs b/src/steam/remote_storage.rs index 3e6e17759..fc39088e8 100644 --- a/src/steam/remote_storage.rs +++ b/src/steam/remote_storage.rs @@ -15,26 +15,20 @@ impl UgcHandle { client: Client, location: impl Into>, priority: u32, - ) -> impl Future> + Send + Sync - { + ) -> impl Future> + Send { let location = CString::new(location.into()); async move { let location = location.context(error::Nul)?; let response: sys::RemoteStorageDownloadUGCResult_t = unsafe { - client - .future_from_call_result_fn( - sys::RemoteStorageDownloadUGCResult_t_k_iCallback, - || { - sys::SteamAPI_ISteamRemoteStorage_UGCDownloadToLocation( - client.0.remote_storage, - self.0, - location.as_ptr(), - priority, - ) - }, - ) - .await + let handle = sys::SteamAPI_ISteamRemoteStorage_UGCDownloadToLocation( + *client.0.remote_storage, + self.0, + location.as_ptr(), + priority, + ); + + client.register_for_call_result(handle).await }; { diff --git a/src/steam/ugc.rs b/src/steam/ugc.rs index fc67ab3b6..44b7a4354 100644 --- a/src/steam/ugc.rs +++ b/src/steam/ugc.rs @@ -389,13 +389,11 @@ impl QueryAllUgc { None => ptr::null(), }; sys::SteamAPI_ISteamUGC_CreateQueryAllUGCRequestCursor( - client.0.ugc, + *client.0.ugc, self.query_type.into(), self.matching_ugc_type.into(), - self.creator_app_id.unwrap_or_else(|| current_app_id).into(), - self.consumer_app_id - .unwrap_or_else(|| current_app_id) - .into(), + self.creator_app_id.unwrap_or(current_app_id).into(), + self.consumer_app_id.unwrap_or(current_app_id).into(), pointer, ) }; @@ -406,14 +404,14 @@ impl QueryAllUgc { unsafe { let success = sys::SteamAPI_ISteamUGC_SetReturnLongDescription( - client.0.ugc, + *client.0.ugc, handle, self.return_long_description, ); assert!(success, "SetReturnLongDescription failed"); let success = sys::SteamAPI_ISteamUGC_SetMatchAnyTag( - client.0.ugc, + *client.0.ugc, handle, self.match_any_tag, ); @@ -422,13 +420,13 @@ impl QueryAllUgc { for (tag, required) in &self.tags { if *required { sys::SteamAPI_ISteamUGC_AddRequiredTag( - client.0.ugc, + *client.0.ugc, handle, tag.as_ptr(), ); } else { sys::SteamAPI_ISteamUGC_AddExcludedTag( - client.0.ugc, + *client.0.ugc, handle, tag.as_ptr(), ); @@ -436,15 +434,11 @@ impl QueryAllUgc { } } - let response: sys::SteamUGCQueryCompleted_t = self - .client - .future_from_call_result_fn( - sys::SteamUGCQueryCompleted_t_k_iCallback, - || unsafe { - sys::SteamAPI_ISteamUGC_SendQueryUGCRequest(client.0.ugc, handle) - }, - ) - .await; + let response: sys::SteamUGCQueryCompleted_t = unsafe { + let handle = sys::SteamAPI_ISteamUGC_SendQueryUGCRequest(*client.0.ugc, handle); + + self.client.register_for_call_result(handle).await + }; { let result = SteamResult::from_inner(response.m_eResult); @@ -465,7 +459,7 @@ impl QueryAllUgc { let mut details: MaybeUninit = MaybeUninit::uninit(); let success = unsafe { sys::SteamAPI_ISteamUGC_GetQueryUGCResult( - client.0.ugc, + *client.0.ugc, response.m_handle, i, details.as_mut_ptr(), @@ -476,7 +470,7 @@ impl QueryAllUgc { let preview_url = unsafe { let mut buf = vec![0_u8; 256]; sys::SteamAPI_ISteamUGC_GetQueryUGCPreviewURL( - client.0.ugc, + *client.0.ugc, response.m_handle, i, buf.as_mut_ptr() as *mut c_char, @@ -530,7 +524,7 @@ impl QueryAllUgc { details_returned += 1; } - unsafe { sys::SteamAPI_ISteamUGC_ReleaseQueryUGCRequest(client.0.ugc, handle) }; + unsafe { sys::SteamAPI_ISteamUGC_ReleaseQueryUGCRequest(*client.0.ugc, handle) }; let more_items_wanted = items_to_reach_quota > 0; let more_items_available = response.m_unTotalMatchingResults > details_returned; diff --git a/src/steam/user_stats.rs b/src/steam/user_stats.rs index e67914330..77160298c 100644 --- a/src/steam/user_stats.rs +++ b/src/steam/user_stats.rs @@ -6,6 +6,7 @@ use crate::{ Client, }; use futures::{lock::Mutex, Future}; +use futures_intrusive::sync::Semaphore; use once_cell::sync::Lazy; use snafu::{ensure, ResultExt}; use std::{cmp, convert::TryInto, ffi::CString, mem::MaybeUninit, ptr}; @@ -38,7 +39,7 @@ impl LeaderboardHandle { range_start: u32, range_end: u32, max_details: u8, - ) -> impl Future> + Send + Sync + '_ { + ) -> impl Future> + Send + '_ { assert!(range_start > 0); assert!(range_end >= range_start); @@ -64,7 +65,7 @@ impl LeaderboardHandle { range_start: i32, range_end: i32, max_details: u8, - ) -> impl Future> + Send + Sync + '_ { + ) -> impl Future> + Send + '_ { assert!(range_end >= range_start); self.download_entry_range( @@ -81,7 +82,7 @@ impl LeaderboardHandle { pub fn download_friends( &self, max_details: u8, - ) -> impl Future> + Send + Sync + '_ { + ) -> impl Future> + Send + '_ { self.download_entry_range( sys::ELeaderboardDataRequest_k_ELeaderboardDataRequestFriends, 0, @@ -132,22 +133,18 @@ impl LeaderboardHandle { async move { let _guard = LOCK.lock().await; - let response: sys::LeaderboardScoreUploaded_t = self - .client - .future_from_call_result_fn( - sys::LeaderboardScoreUploaded_t_k_iCallback, - || unsafe { - sys::SteamAPI_ISteamUserStats_UploadLeaderboardScore( - self.client.0.user_stats, - self.handle, - leaderboard_upload_score_method, - score, - details.map(|xs| xs.as_ptr()).unwrap_or(ptr::null()), - details_count, - ) - }, - ) - .await; + let response: sys::LeaderboardScoreUploaded_t = unsafe { + let handle = sys::SteamAPI_ISteamUserStats_UploadLeaderboardScore( + *self.client.0.user_stats, + self.handle, + leaderboard_upload_score_method, + score, + details.map(|xs| xs.as_ptr()).unwrap_or(ptr::null()), + details_count, + ); + + self.client.register_for_call_result(handle).await + }; if response.m_bSuccess == 1 { Ok(LeaderboardScoreUploaded { @@ -167,24 +164,20 @@ impl LeaderboardHandle { range_start: i32, range_end: i32, max_details: u8, - ) -> impl Future> + Send + Sync + '_ { + ) -> impl Future> + Send + '_ { let max_details = cmp::min(max_details, 64); async move { - let response: sys::LeaderboardScoresDownloaded_t = self - .client - .future_from_call_result_fn( - sys::LeaderboardScoresDownloaded_t_k_iCallback, - || unsafe { - sys::SteamAPI_ISteamUserStats_DownloadLeaderboardEntries( - self.client.0.user_stats, - self.handle, - request_type, - range_start, - range_end, - ) - }, - ) - .await; + let response: sys::LeaderboardScoresDownloaded_t = unsafe { + let handle = sys::SteamAPI_ISteamUserStats_DownloadLeaderboardEntries( + *self.client.0.user_stats, + self.handle, + request_type, + range_start, + range_end, + ); + + self.client.register_for_call_result(handle).await + }; let mut entries: Vec = Vec::with_capacity(response.m_cEntryCount as usize); @@ -193,7 +186,7 @@ impl LeaderboardHandle { let mut details = vec![0; max_details as usize]; let success = unsafe { sys::SteamAPI_ISteamUserStats_GetDownloadedLeaderboardEntry( - self.client.0.user_stats, + *self.client.0.user_stats, response.m_hSteamLeaderboardEntries, i, raw_entry.as_mut_ptr(), @@ -258,7 +251,7 @@ mod error { TooLong { length: usize }, /// The specified leaderboard was not found - #[snafu(display("The leaderboard '{:?}' was not found", leaderboard_name))] + #[snafu(display("The leaderboard {:?} was not found", leaderboard_name))] NotFound { leaderboard_name: std::ffi::CString }, } @@ -280,7 +273,12 @@ mod error { pub(crate) fn find_leaderboard( client: &Client, leaderboard_name: Vec, -) -> impl Future> + Send + Sync + '_ { +) -> impl Future> + Send + '_ { + // The Steamworks API seems to have an undocumented limit on the number of concurrent calls + // to the `FindLeaderboard()` function, after which it starts returning leaderboard-not-found + // errors. So we limit the number of concurrent calls to an experimentally-determined value. + static SEMAPHORE: Lazy = Lazy::new(|| Semaphore::new(false, 256)); + let leaderboard_name = CString::new(leaderboard_name); async move { let leaderboard_name = leaderboard_name.context(error::Nul)?; @@ -292,14 +290,15 @@ pub(crate) fn find_leaderboard( } ); - let response: sys::LeaderboardFindResult_t = client - .future_from_call_result_fn(sys::LeaderboardFindResult_t_k_iCallback, || unsafe { - sys::SteamAPI_ISteamUserStats_FindLeaderboard( - client.0.user_stats, - leaderboard_name_bytes.as_ptr() as *const i8, - ) - }) - .await; + let _releaser = SEMAPHORE.acquire(1).await; + let response: sys::LeaderboardFindResult_t = unsafe { + let handle = sys::SteamAPI_ISteamUserStats_FindLeaderboard( + *client.0.user_stats, + leaderboard_name_bytes.as_ptr() as *const i8, + ); + + client.register_for_call_result(handle).await + }; ensure!( response.m_bLeaderboardFound != 0, diff --git a/steamworks-sys/Cargo.toml b/steamworks-sys/Cargo.toml index 29bb143d6..40cb29d06 100644 --- a/steamworks-sys/Cargo.toml +++ b/steamworks-sys/Cargo.toml @@ -6,5 +6,4 @@ edition = "2018" [build-dependencies] bindgen = "0.54" -cc = "1" dunce = "1" diff --git a/steamworks-sys/build.rs b/steamworks-sys/build.rs index 39de1638b..3d76caed4 100644 --- a/steamworks-sys/build.rs +++ b/steamworks-sys/build.rs @@ -21,47 +21,44 @@ fn main() { let triple = env::var("TARGET").unwrap(); let mut lib = "steam_api"; - let (path, runtime_dependency) = if triple.contains("windows") { + let (path, lib_files): (_, &[&str]) = if triple.contains("windows") { if triple.contains("i686") { - (sdk_loc.join("redistributable_bin"), "steam_api.dll") + ( + sdk_loc.join("redistributable_bin"), + &["steam_api.dll", "steam_api.lib"], + ) } else { lib = "steam_api64"; - (sdk_loc.join("redistributable_bin/win64"), "steam_api64.dll") + ( + sdk_loc.join("redistributable_bin/win64"), + &["steam_api64.dll", "steam_api64.lib"], + ) } } else if triple.contains("linux") { if triple.contains("i686") { ( sdk_loc.join("redistributable_bin/linux32"), - "libsteam_api.so", + &["libsteam_api.so"], ) } else { ( sdk_loc.join("redistributable_bin/linux64"), - "libsteam_api.so", + &["libsteam_api.so"], ) } } else if triple.contains("darwin") { ( sdk_loc.join("redistributable_bin/osx"), - "libsteam_api.dylib", + &["libsteam_api.dylib"], ) } else { panic!("Unsupported OS"); }; - println!("cargo:rustc-link-lib=dylib={}", lib); - println!("cargo:rustc-link-search={}", path.display()); - - cc::Build::new() - .cpp(true) - .flag_if_supported("-std=c++11") - .include(sdk_loc.join("public")) - .file("src/lib.cpp") - .compile("steamrust"); + for file in lib_files { + fs::copy(path.join(file), out_path.join(file)).unwrap(); + } - fs::copy( - path.join(runtime_dependency), - out_path.join(runtime_dependency), - ) - .unwrap(); + println!("cargo:rustc-link-lib=dylib={}", lib); + println!("cargo:rustc-link-search={}", out_path.display()); } diff --git a/steamworks-sys/src/lib.cpp b/steamworks-sys/src/lib.cpp deleted file mode 100644 index 33ed1acc4..000000000 --- a/steamworks-sys/src/lib.cpp +++ /dev/null @@ -1,30 +0,0 @@ -#include "../wrapper.hpp" - -CallbackManager::CallbackManager(SteamRustCallbacks callbacks): callbacks(callbacks) { - this->persona_state_change_registration.Register(this, &CallbackManager::OnPersonaStateChange); - this->steam_shutdown_registration.Register(this, &CallbackManager::OnSteamShutdown); -} - -CallbackManager::~CallbackManager() { - this->persona_state_change_registration.Unregister(); - this->steam_shutdown_registration.Unregister(); -} - -void CallbackManager::OnPersonaStateChange(PersonaStateChange_t* pCallback) -{ - this->callbacks.onPersonaStateChanged(pCallback); -} - -void CallbackManager::OnSteamShutdown(SteamShutdown_t* pCallback) -{ - this->callbacks.onSteamShutdown(pCallback); -} - -void steam_rust_unregister_callbacks(CallbackManager* manager) { - delete manager; -} - -CallbackManager* steam_rust_register_callbacks(SteamRustCallbacks callbacks) { - auto manager = new CallbackManager(callbacks); - return manager; -} diff --git a/steamworks-sys/wrapper.hpp b/steamworks-sys/wrapper.hpp index b12de7300..461a6dadd 100644 --- a/steamworks-sys/wrapper.hpp +++ b/steamworks-sys/wrapper.hpp @@ -1,22 +1 @@ #include "steam/steam_api_flat.h" - -typedef struct { - void (*onPersonaStateChanged)(PersonaStateChange_t*); - void (*onSteamShutdown)(SteamShutdown_t*); -} SteamRustCallbacks; - -class CallbackManager -{ -public: - explicit CallbackManager(SteamRustCallbacks callbacks); - ~CallbackManager(); - -private: - SteamRustCallbacks callbacks; - - STEAM_CALLBACK_MANUAL(CallbackManager, OnPersonaStateChange, PersonaStateChange_t, persona_state_change_registration); - STEAM_CALLBACK_MANUAL(CallbackManager, OnSteamShutdown, SteamShutdown_t, steam_shutdown_registration); -}; - -CallbackManager* steam_rust_register_callbacks(SteamRustCallbacks); -void steam_rust_unregister_callbacks(CallbackManager*);