bevy_steamworks/
lib.rs

1#![deny(missing_docs)]
2
3//! This crate provides a [Bevy](https://bevyengine.org/) plugin for integrating with
4//! the Steamworks SDK.
5//!
6//! The underlying steamworks crate comes bundled with the redistributable dynamic
7//! libraries a compatible version of the SDK. Currently it's v153a.
8//!
9//! ## Usage
10//!
11//! To add the plugin to your app, simply add the `SteamworksPlugin` to your
12//! `App`. This will require the `AppId` provided to you by Valve for initialization.
13//!
14//! ```rust no_run
15//! use bevy::prelude::*;
16//! use bevy_steamworks::*;
17//!
18//! fn main() {
19//!   // Use the demo Steam AppId for SpaceWar
20//!   App::new()
21//!       // it is important to add the plugin before `RenderPlugin` that comes with `DefaultPlugins`
22//!       .add_plugins(SteamworksPlugin::init_app(480).unwrap())
23//!       .add_plugins(DefaultPlugins)
24//!       .run();
25//! }
26//! ```
27//!
28//! The plugin adds `Client` as a Bevy ECS resource, which can be
29//! accessed like any other resource in Bevy. The client implements `Send` and `Sync`
30//! and can be used to make requests via the SDK from any of Bevy's threads.
31//!
32//! The plugin will automatically call `SingleClient::run_callbacks` on the Bevy
33//! every tick in the `First` schedule, so there is no need to run it manually.  
34//!
35//! All callbacks are forwarded as `Events` and can be listened to in the a
36//! Bevy idiomatic way:
37//!
38//! ```rust no_run
39//! use bevy::prelude::*;
40//! use bevy_steamworks::*;
41//!
42//! fn steam_system(steam_client: Res<Client>) {
43//!   for friend in steam_client.friends().get_friends(FriendFlags::IMMEDIATE) {
44//!     println!("Friend: {:?} - {}({:?})", friend.id(), friend.name(), friend.state());
45//!   }
46//! }
47//!
48//! fn main() {
49//!   // Use the demo Steam AppId for SpaceWar
50//!   App::new()
51//!       // it is important to add the plugin before `RenderPlugin` that comes with `DefaultPlugins`
52//!       .add_plugins(SteamworksPlugin::init_app(480).unwrap())
53//!       .add_plugins(DefaultPlugins)
54//!       .add_systems(Startup, steam_system)
55//!       .run();
56//! }
57//! ```
58
59use std::{
60    ops::Deref,
61    sync::{Arc, Mutex},
62};
63
64use bevy_app::{App, First, Plugin};
65use bevy_ecs::{
66    event::EventWriter,
67    prelude::Event,
68    schedule::*,
69    system::{Res, ResMut, Resource},
70};
71use bevy_utils::{synccell::SyncCell, syncunsafecell::SyncUnsafeCell};
72// Reexport everything from steamworks except for the clients
73pub use steamworks::{
74    networking_messages, networking_sockets, networking_utils, restart_app_if_necessary, AccountId,
75    AppIDs, AppId, Apps, AuthSessionError, AuthSessionTicketResponse, AuthSessionValidateError,
76    AuthTicket, Callback, CallbackHandle, ChatMemberStateChange, ComparisonFilter,
77    CreateQueryError, DistanceFilter, DownloadItemResult, FileType,
78    FloatingGamepadTextInputDismissed, FloatingGamepadTextInputMode, Friend, FriendFlags,
79    FriendGame, FriendState, Friends, GameId, GameLobbyJoinRequested, GameOverlayActivated,
80    GamepadTextInputDismissed, GamepadTextInputLineMode, GamepadTextInputMode, Input, InstallInfo,
81    InvalidErrorCode, ItemState, Leaderboard, LeaderboardDataRequest, LeaderboardDisplayType,
82    LeaderboardEntry, LeaderboardScoreUploaded, LeaderboardSortMethod, LobbyChatUpdate,
83    LobbyDataUpdate, LobbyId, LobbyKey, LobbyKeyTooLongError, LobbyListFilter, LobbyType, Manager,
84    Matchmaking, MicroTxnAuthorizationResponse, NearFilter, NearFilters, Networking,
85    NotificationPosition, NumberFilter, NumberFilters, OverlayToStoreFlag, P2PSessionConnectFail,
86    P2PSessionRequest, PersonaChange, PersonaStateChange, PublishedFileId, PublishedFileVisibility,
87    QueryHandle, QueryResult, QueryResults, RemotePlay, RemotePlayConnected,
88    RemotePlayDisconnected, RemotePlaySession, RemotePlaySessionId, RemoteStorage, SIResult,
89    SResult, SendType, Server, ServerManager, ServerMode, SteamAPIInitError, SteamDeviceFormFactor,
90    SteamError, SteamFile, SteamFileInfo, SteamFileReader, SteamFileWriter, SteamId,
91    SteamServerConnectFailure, SteamServersConnected, SteamServersDisconnected, StringFilter,
92    StringFilterKind, StringFilters, TicketForWebApiResponse, UGCContentDescriptorID, UGCQueryType,
93    UGCStatisticType, UGCType, UpdateHandle, UpdateStatus, UpdateWatchHandle, UploadScoreMethod,
94    User, UserAchievementStored, UserList, UserListOrder, UserRestriction, UserStats,
95    UserStatsReceived, UserStatsStored, Utils, ValidateAuthTicketResponse, RESULTS_PER_PAGE, UGC,
96};
97
98#[derive(Resource)]
99struct SteamEvents {
100    _callbacks: Vec<CallbackHandle>,
101    pending: Arc<SyncUnsafeCell<Vec<SteamworksEvent>>>,
102}
103
104/// A Bevy-compatible wrapper around various Steamworks events.
105#[derive(Event)]
106#[allow(missing_docs)]
107pub enum SteamworksEvent {
108    AuthSessionTicketResponse(steamworks::AuthSessionTicketResponse),
109    DownloadItemResult(steamworks::DownloadItemResult),
110    GameLobbyJoinRequested(steamworks::GameLobbyJoinRequested),
111    LobbyChatUpdate(steamworks::LobbyChatUpdate),
112    P2PSessionConnectFail(steamworks::P2PSessionConnectFail),
113    P2PSessionRequest(steamworks::P2PSessionRequest),
114    PersonaStateChange(steamworks::PersonaStateChange),
115    SteamServerConnectFailure(steamworks::SteamServerConnectFailure),
116    SteamServersConnected(steamworks::SteamServersConnected),
117    SteamServersDisconnected(steamworks::SteamServersDisconnected),
118    TicketForWebApiResponse(steamworks::TicketForWebApiResponse),
119    UserAchievementStored(steamworks::UserAchievementStored),
120    UserStatsReceived(steamworks::UserStatsReceived),
121    UserStatsStored(steamworks::UserStatsStored),
122    ValidateAuthTicketResponse(steamworks::ValidateAuthTicketResponse),
123}
124
125macro_rules! register_event_callbacks {
126    ($client: ident, $($event_name: ident),+) => {
127        {
128            let pending = Arc::new(SyncUnsafeCell::new(Vec::new()));
129            SteamEvents {
130                _callbacks: vec![
131                    $({
132                        let pending_in = pending.clone();
133                        $client.register_callback::<steamworks::$event_name, _>(move |evt| {
134                            // SAFETY: The callback is only called during `run_steam_callbacks` which cannot run
135                            // while any of the flush_events systems are running. This cannot alias.
136                            unsafe {
137                                (&mut *pending_in.get()).push(SteamworksEvent::$event_name(evt));
138                            }
139                        })
140                    }),+
141                ],
142                pending,
143            }
144        }
145    };
146}
147
148/// A Bevy compatible wrapper around [`steamworks::Client`].
149///
150/// Automatically dereferences to the client so it can be transparently
151/// used.
152///
153/// For more information on how to use it, see [`steamworks::Client`].
154#[derive(Resource, Clone)]
155pub struct Client(steamworks::Client);
156
157impl Deref for Client {
158    type Target = steamworks::Client;
159    fn deref(&self) -> &Self::Target {
160        &self.0
161    }
162}
163
164#[derive(Resource)]
165struct SingleClient(SyncCell<steamworks::SingleClient>);
166
167/// A Bevy [`Plugin`] for adding support for the Steam SDK.
168pub struct SteamworksPlugin {
169    steam: Mutex<Option<(steamworks::Client, steamworks::SingleClient)>>,
170}
171
172impl SteamworksPlugin {
173    /// Creates a new `SteamworksPlugin`. The provided `app_id` should correspond
174    /// to the Steam app ID provided by Valve.
175    pub fn init_app(app_id: impl Into<AppId>) -> Result<Self, SteamAPIInitError> {
176        Ok(Self {
177            steam: Mutex::new(Some(steamworks::Client::init_app(app_id.into())?)),
178        })
179    }
180
181    /// Creates a new `SteamworksPlugin` using the automatically determined app ID.
182    /// If the game isn't being run through steam this can be provided by placing a steam_appid.txt
183    /// with the ID inside in the current working directory.
184    /// Alternatively, you can use `SteamworksPlugin::init_app(<app_id>)` to force a specific app ID.
185    pub fn init() -> Result<Self, SteamAPIInitError> {
186        Ok(Self {
187            steam: Mutex::new(Some(steamworks::Client::init()?)),
188        })
189    }
190}
191
192impl Plugin for SteamworksPlugin {
193    fn build(&self, app: &mut App) {
194        let (client, single) = self
195            .steam
196            .lock()
197            .unwrap()
198            .take()
199            .expect("The SteamworksPlugin was initialized more than once");
200
201        app.insert_resource(Client(client.clone()))
202            .insert_resource(SingleClient(SyncCell::new(single)))
203            .insert_resource(register_event_callbacks!(
204                client,
205                AuthSessionTicketResponse,
206                DownloadItemResult,
207                GameLobbyJoinRequested,
208                LobbyChatUpdate,
209                P2PSessionConnectFail,
210                P2PSessionRequest,
211                PersonaStateChange,
212                SteamServerConnectFailure,
213                SteamServersConnected,
214                SteamServersDisconnected,
215                TicketForWebApiResponse,
216                UserAchievementStored,
217                UserStatsReceived,
218                UserStatsStored,
219                ValidateAuthTicketResponse
220            ))
221            .add_event::<SteamworksEvent>()
222            .configure_sets(First, SteamworksSystem::RunCallbacks)
223            .add_systems(
224                First,
225                run_steam_callbacks
226                    .in_set(SteamworksSystem::RunCallbacks)
227                    .before(bevy_ecs::event::EventUpdates),
228            );
229    }
230}
231
232/// A set of [`SystemSet`]s for systems used by [`SteamworksPlugin`]
233///
234/// [`SystemSet`]: bevy_ecs::schedule::SystemSet
235#[derive(Debug, Clone, Copy, Eq, Hash, SystemSet, PartialEq)]
236pub enum SteamworksSystem {
237    /// A system set that runs the Steam SDK callbacks. Anything dependent on
238    /// Steam API results should scheduled after this. This runs in
239    /// [`First`].
240    RunCallbacks,
241}
242
243fn run_steam_callbacks(
244    mut client: ResMut<SingleClient>,
245    events: Res<SteamEvents>,
246    mut output: EventWriter<SteamworksEvent>,
247) {
248    client.0.get().run_callbacks();
249    // SAFETY: The callback is only called during `run_steam_callbacks` which cannot run
250    // while any of the flush_events systems are running. The system is registered only once for
251    // the client. This cannot alias.
252    let pending = unsafe { &mut *events.pending.get() };
253    if !pending.is_empty() {
254        output.send_batch(pending.drain(0..));
255    }
256}