Skip to content

Commit

Permalink
[WIP] Switch to manual dispatch API
Browse files Browse the repository at this point in the history
Also,
- Update deps
- Add debug hook
- Tweak build script
- Add logging through the tracing crate
  • Loading branch information
Seeker14491 committed Sep 9, 2020
1 parent 1144422 commit c7cc265
Show file tree
Hide file tree
Showing 13 changed files with 413 additions and 292 deletions.
9 changes: 6 additions & 3 deletions Cargo.toml
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", "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"
51 changes: 45 additions & 6 deletions 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)']
Expand Down
123 changes: 93 additions & 30 deletions 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<T> =
Lazy<Mutex<DenseSlotMap<slotmap::DefaultKey, futures::channel::mpsc::UnboundedSender<T>>>>;
Mutex<DenseSlotMap<slotmap::DefaultKey, futures::channel::mpsc::UnboundedSender<T>>>;

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
}

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

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

pub(crate) static PERSONA_STATE_CHANGED: CallbackStorage<PersonaStateChange> =
Lazy::new(|| Mutex::new(DenseSlotMap::new()));
// TODO: macro to write these implementations
#[derive(Debug, Default)]
pub(crate) struct PersonaStateChangeDispatcher(CallbackStorage<PersonaStateChange>);

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<PersonaStateChange> {
&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<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<()> {
&self.0
}

for k in &keys_to_remove {
map.remove(*k);
}
fn map_callback_data(_raw: &sys::SteamShutdown_t) {}
}

0 comments on commit c7cc265

Please sign in to comment.