Skip to main content

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, AccountType, AppIDs, AppId, Apps, AuthSessionError,
74    AuthSessionTicketResponse, AuthSessionValidateError, AuthTicket, Callback, CallbackHandle,
75    CallbackResult, ChatEntryType, ChatMemberStateChange, ChatRoomEnterResponse, ComparisonFilter,
76    CreateQueryError, DistanceFilter, DownloadItemResult, FileType,
77    FloatingGamepadTextInputDismissed, FloatingGamepadTextInputMode, Friend, FriendFlags,
78    FriendGame, FriendState, Friends, GameId, GameLobbyJoinRequested, GameOverlayActivated,
79    GamepadTextInputDismissed, GamepadTextInputLineMode, GamepadTextInputMode, Input, InstallInfo,
80    InvalidErrorCode, ItemState, Leaderboard, LeaderboardDataRequest, LeaderboardDisplayType,
81    LeaderboardEntry, LeaderboardScoreUploaded, LeaderboardSortMethod, LobbyChatMsg,
82    LobbyChatUpdate, LobbyCreated, LobbyDataUpdate, LobbyEnter, LobbyId, LobbyKey,
83    LobbyKeyTooLongError, LobbyListFilter, LobbyType, Matchmaking, MicroTxnAuthorizationResponse,
84    NearFilter, NearFilters, Networking, NotificationPosition, NumberFilter, NumberFilters,
85    OverlayToStoreFlag, P2PSessionConnectFail, P2PSessionRequest, PersonaChange,
86    PersonaStateChange, PublishedFileId, PublishedFileVisibility, QueryHandle, QueryResult,
87    QueryResults, RemotePlay, RemotePlayConnected, RemotePlayDisconnected, RemotePlaySession,
88    RemotePlaySessionId, RemoteStorage, SIResult, SResult, SendType, Server, ServerMode,
89    SteamAPIInitError, SteamDeviceFormFactor, SteamError, SteamFile, SteamFileInfo,
90    SteamFileReader, SteamFileWriter, SteamId, SteamServerConnectFailure, SteamServersConnected,
91    SteamServersDisconnected, StringFilter, StringFilterKind, StringFilters,
92    TicketForWebApiResponse, UGCContentDescriptorID, UGCQueryType, UGCStatisticType, UGCType,
93    Universe, UpdateHandle, UpdateStatus, UpdateWatchHandle, UploadScoreMethod, User,
94    UserAchievementStored, UserList, UserListOrder, UserStats, UserStatsReceived, UserStatsStored,
95    Utils, ValidateAuthTicketResponse, RESULTS_PER_PAGE, UGC,
96};
97
98/// A Bevy-compatible wrapper around various Steamworks events.
99#[derive(Message, Debug)]
100#[allow(missing_docs)]
101pub enum SteamworksEvent {
102    CallbackResult(CallbackResult),
103}
104
105/// A Bevy compatible wrapper around [`steamworks::Client`].
106///
107/// Automatically dereferences to the client so it can be transparently
108/// used.
109///
110/// For more information on how to use it, see [`steamworks::Client`].
111#[derive(Resource, Clone)]
112pub struct Client(steamworks::Client);
113
114impl Deref for Client {
115    type Target = steamworks::Client;
116    fn deref(&self) -> &Self::Target {
117        &self.0
118    }
119}
120
121/// A Bevy [`Plugin`] for adding support for the Steam SDK.
122pub struct SteamworksPlugin {
123    steam: Mutex<Option<steamworks::Client>>,
124}
125
126impl SteamworksPlugin {
127    /// Creates a new `SteamworksPlugin`. The provided `app_id` should correspond
128    /// to the Steam app ID provided by Valve.
129    pub fn init_app(app_id: impl Into<AppId>) -> Result<Self, SteamAPIInitError> {
130        Ok(Self {
131            steam: Mutex::new(Some(steamworks::Client::init_app(app_id.into())?)),
132        })
133    }
134
135    /// Creates a new `SteamworksPlugin` using the automatically determined app ID.
136    /// If the game isn't being run through steam this can be provided by placing a steam_appid.txt
137    /// with the ID inside in the current working directory.
138    /// Alternatively, you can use `SteamworksPlugin::init_app(<app_id>)` to force a specific app ID.
139    pub fn init() -> Result<Self, SteamAPIInitError> {
140        Ok(Self {
141            steam: Mutex::new(Some(steamworks::Client::init()?)),
142        })
143    }
144}
145
146impl From<steamworks::Client> for SteamworksPlugin {
147    fn from(client: steamworks::Client) -> Self {
148        Self {
149            steam: Mutex::new(Some(client)),
150        }
151    }
152}
153
154impl Plugin for SteamworksPlugin {
155    fn build(&self, app: &mut App) {
156        let client = self
157            .steam
158            .lock()
159            .unwrap()
160            .take()
161            .expect("The SteamworksPlugin was initialized more than once");
162
163        app.insert_resource(Client(client.clone()))
164            .add_message::<SteamworksEvent>()
165            .configure_sets(First, SteamworksSystem::RunCallbacks)
166            .add_systems(
167                First,
168                run_steam_callbacks
169                    .in_set(SteamworksSystem::RunCallbacks)
170                    .before(bevy_ecs::message::MessageUpdateSystems),
171            );
172    }
173}
174
175/// A set of [`SystemSet`]s for systems used by [`SteamworksPlugin`]
176///
177/// [`SystemSet`]: bevy_ecs::schedule::SystemSet
178#[derive(Debug, Clone, Copy, Eq, Hash, SystemSet, PartialEq)]
179pub enum SteamworksSystem {
180    /// A system set that runs the Steam SDK callbacks. Anything dependent on
181    /// Steam API results should scheduled after this. This runs in
182    /// [`First`].
183    RunCallbacks,
184}
185
186fn run_steam_callbacks(client: Res<Client>, mut output: MessageWriter<SteamworksEvent>) {
187    client.process_callbacks(|callback| {
188        output.write(SteamworksEvent::CallbackResult(callback));
189    });
190}