Skip to content

Commit

Permalink
Switch to manual dispatch API + general overhaul
Browse files Browse the repository at this point in the history
Non-exhaustive list of additional changes:
- Increase reliability when fetching a huge number of leaderboards concurrently, by limiting the number of concurrent requests
- Add logging using the tracing crate
- The build script was changed so we now link to the copy of the Steamworks dynamic library the build script makes. Previously we linked to the original files in the redistributagle_bin directory.
- Previously, the `ClientInner` struct had an unsafe impl for `Send` and `Sync`, because it directly held a raw pointer for each Steamworks interface, which aren't `Send` and `Sync`. Now, we instead wrap each pointer in a newtype with the unsafe impls for `Send` and `Sync`. This is safer, because the compiler will make sure everything else in the `ClientInner` struct is `Send` and `Sync`. We also add a static assertion that the `Client` struct is `Send` and `Sync`.
- No longer use or depend on an async executor
- Use Steamwork's `ISteamUtils::SetWarningMessageHook` to log messages and errors received from the Steam API
- Remove `Sync` bound on returned `impl Future`s for consistency, as it's not usefull, and can't always be satisfied
- Update README and add an example
- Update deps

closes #2
  • Loading branch information
Seeker14491 committed Sep 11, 2020
1 parent c10fa1e commit 1a45a04
Show file tree
Hide file tree
Showing 14 changed files with 435 additions and 309 deletions.
9 changes: 6 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
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"
51 changes: 45 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -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)']
Expand Down
92 changes: 92 additions & 0 deletions src/callbacks/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
mod persona_state_change;

pub use persona_state_change::*;

use az::WrappingCast;
use futures::Stream;
use parking_lot::Mutex;
use slotmap::DenseSlotMap;
use std::{convert::TryFrom, mem};
use steamworks_sys as sys;

pub(crate) type CallbackStorage<T> =
Mutex<DenseSlotMap<slotmap::DefaultKey, futures::channel::mpsc::UnboundedSender<T>>>;

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),
_ => {}
}
}

pub(crate) fn register_to_receive_callback<T: Clone + Send + 'static>(
dispatcher: &impl CallbackDispatcher<MappedCallbackData = T>,
) -> impl Stream<Item = T> + Send {
let (tx, rx) = futures::channel::mpsc::unbounded();
dispatcher.storage().lock().insert(tx);
rx
}

#[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<Self::MappedCallbackData>;
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::<Self::RawCallbackData>()),
0
);
assert_eq!(
usize::try_from(callback_data_len).unwrap(),
mem::size_of::<Self::RawCallbackData>()
);

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, Default)]
pub(crate) struct SteamShutdownDispatcher(CallbackStorage<()>);

impl CallbackDispatcher for SteamShutdownDispatcher {
type RawCallbackData = sys::SteamShutdown_t;
type MappedCallbackData = ();

fn storage(&self) -> &CallbackStorage<()> {
&self.0
}

fn map_callback_data(_raw: &sys::SteamShutdown_t) {}
}
54 changes: 16 additions & 38 deletions src/callbacks.rs → src/callbacks/persona_state_change.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
use crate::steam::SteamId;
use crate::{
callbacks::{CallbackDispatcher, CallbackStorage},
steam::SteamId,
};
use bitflags::bitflags;
use once_cell::sync::Lazy;
use parking_lot::Mutex;
use slotmap::DenseSlotMap;
use steamworks_sys as sys;

pub(crate) type CallbackStorage<T> =
Lazy<Mutex<DenseSlotMap<slotmap::DefaultKey, futures::channel::mpsc::UnboundedSender<T>>>>;

/// <https://partner.steamgames.com/doc/api/ISteamFriends#PersonaStateChange_t>
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)]
pub struct PersonaStateChange {
Expand Down Expand Up @@ -36,40 +33,21 @@ bitflags! {
}
}

pub(crate) static PERSONA_STATE_CHANGED: CallbackStorage<PersonaStateChange> =
Lazy::new(|| Mutex::new(DenseSlotMap::new()));

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),
};

forward_callback(&PERSONA_STATE_CHANGED, params);
}

pub(crate) static STEAM_SHUTDOWN: CallbackStorage<()> =
Lazy::new(|| Mutex::new(DenseSlotMap::new()));
#[derive(Debug, Default)]
pub(crate) struct PersonaStateChangeDispatcher(CallbackStorage<PersonaStateChange>);

pub(crate) unsafe extern "C" fn on_steam_shutdown(_: *mut sys::SteamShutdown_t) {
forward_callback(&STEAM_SHUTDOWN, ());
}
impl CallbackDispatcher for PersonaStateChangeDispatcher {
type RawCallbackData = sys::PersonaStateChange_t;
type MappedCallbackData = PersonaStateChange;

fn forward_callback<T: Copy + Send + 'static>(storage: &CallbackStorage<T>, 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<PersonaStateChange> {
&self.0
}

for k in &keys_to_remove {
map.remove(*k);
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),
}
}
}
Loading

0 comments on commit 1a45a04

Please sign in to comment.