bevy_steamworks/
lib.rs

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