1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
#![deny(missing_docs)]

//! This crate provides a [Bevy](https://bevyengine.org/) plugin for integrating with
//! the Steamworks SDK.
//!
//! ## Bevy Version Supported
//!
//! |Bevy Version |bevy\_steamworks|
//! |:------------|:---------------|
//! |git (main)   |git (develop)   |
//! |0.8          |0.5             |
//! |0.7          |0.4             |
//! |0.6          |0.2, 0.3        |
//! |0.5          |0.1             |
//!
//! ## Installation
//! Add the following to your `Cargo.toml`:
//!
//! ```toml
//! [dependencies]
//! bevy-steamworks = "0.5"
//! ```
//!
//! The steamworks crate comes bundled with the redistributable dynamic libraries
//! of a compatible version of the SDK. Currently it's v153a.
//!
//! ## Usage
//!
//! To add the plugin to your app, simply add the `SteamworksPlugin` to your
//! `App`. This will require the `AppId` provided to you by Valve for initialization.
//!
//! ```rust no_run
//! use bevy::prelude::*;
//! use bevy_steamworks::*;
//!
//! fn main() {
//!   // Use the demo Steam AppId for SpaceWar
//!   App::new()
//!       .add_plugins(DefaultPlugins)
//!       .add_plugin(SteamworksPlugin::new(AppId(480)))
//!       .run()
//! }
//! ```
//!
//! The plugin adds `steamworks::Client` as a Bevy ECS resource, which can be
//! accessed like any other resource in Bevy. The client implements `Send` and `Sync`
//! and can be used to make requests via the SDK from any of Bevy's threads. However,
//! any asynchronous callbacks from Steam will only run on the main thread.
//!
//! The plugin will automatically call [`SingleClient::run_callbacks`] on the Bevy
//! main thread every frame in [`CoreStage::First`], so there is no need to run it
//! manually.
//!
//! **NOTE**: If the plugin fails to initialize (i.e. `Client::init()` fails and
//! returns an error, an error wil lbe logged (via `bevy_log`), but it will not
//! panic. In this case, it may be necessary to use `Option<Res<Client>>` instead.
//!
//! All callbacks are forwarded as [`Events`] and can be listened to in the a
//! Bevy idiomatic way:
//!
//! ```rust no_run
//! use bevy::prelude::*;
//! use bevy_steamworks::*;
//!
//! fn steam_system(steam_client: Res<Client>) {
//!   for friend in steam_client.friends().get_friends(FriendFlags::IMMEDIATE) {
//!     println!("Friend: {:?} - {}({:?})", friend.id(), friend.name(), friend.state());
//!   }
//! }
//!
//! fn main() {
//!   // Use the demo Steam AppId for SpaceWar
//!   App::new()
//!       .add_plugins(DefaultPlugins)
//!       .add_plugin(SteamworksPlugin::new(AppId(480)))
//!       .add_startup_system(steam_system)
//!       .run()
//! }
//! ```

use bevy_app::{App, CoreStage, Plugin};
use bevy_ecs::{
    event::EventWriter,
    schedule::*,
    system::{NonSend, ResMut, Resource},
};
use parking_lot::Mutex;
use std::{ops::Deref, sync::Arc};

// Reexport everything from steamworks except for the clients
pub use steamworks::{
    networking_messages::*, networking_sockets::*, networking_utils::*, stats::*, AccountId,
    AppIDs, AppId, Apps, AuthSessionError, AuthSessionTicketResponse, AuthSessionValidateError,
    AuthTicket, Callback, CallbackHandle, ChatMemberStateChange, ClientManager, CreateQueryError,
    DownloadItemResult, FileType, Friend, FriendFlags, FriendGame, FriendState, Friends, GameId,
    GameLobbyJoinRequested, InstallInfo, InvalidErrorCode, ItemDetailsQuery, ItemListDetailsQuery,
    ItemState, Leaderboard, LeaderboardDataRequest, LeaderboardDisplayType, LeaderboardEntry,
    LeaderboardScoreUploaded, LeaderboardSortMethod, LobbyChatUpdate, LobbyId, LobbyType,
    Matchmaking, Networking, NotificationPosition, P2PSessionConnectFail, P2PSessionRequest,
    PersonaChange, PersonaStateChange, PublishedFileId, QueryResult, QueryResults, RemoteStorage,
    SResult, SendType, Server, ServerManager, ServerMode, SingleClient, SteamError, SteamFile,
    SteamFileInfo, SteamFileReader, SteamFileWriter, SteamId, SteamServerConnectFailure,
    SteamServersConnected, SteamServersDisconnected, UGCStatisticType, UGCType, UpdateHandle,
    UpdateStatus, UpdateWatchHandle, UploadScoreMethod, User, UserAchievementStored, UserList,
    UserListOrder, UserListQuery, UserStats, UserStatsReceived, UserStatsStored, Utils,
    ValidateAuthTicketResponse, RESULTS_PER_PAGE, UGC,
};

#[derive(Resource)]
struct SteamEvents<T> {
    _callback: CallbackHandle,
    pending: Arc<Mutex<Vec<T>>>,
}

/// A Bevy compatible wrapper around [`steamworks::Client`].
///
/// Automatically dereferences to the client so it can be transparently
/// used.
///
/// For more information on how to use it, see [`steamworks::Client`].
#[derive(Resource, Clone)]
pub struct Client(steamworks::Client);

impl Deref for Client {
    type Target = steamworks::Client;
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

/// A Bevy [`Plugin`] for adding support for the Steam SDK.
///
/// [`Plugin`]: bevy_app::Plugin
pub struct SteamworksPlugin(AppId);

impl SteamworksPlugin {
    /// Creates a new `SteamworksPlugin`. The provided `app_id` should correspond
    /// to the Steam app ID provided by Valve.
    pub fn new(app_id: impl Into<AppId>) -> Self {
        Self(app_id.into())
    }
}

impl Plugin for SteamworksPlugin {
    fn build(&self, app: &mut App) {
        if app.world.contains_resource::<Client>() {
            bevy_log::warn!("Attempted to add the Steamworks plugin multiple times!");
            return;
        }
        match steamworks::Client::init_app(self.0) {
            Err(err) => bevy_log::error!("Failed to initialize Steamworks client: {}", err),
            Ok((client, single)) => {
                app.insert_resource(Client(client.clone()))
                    .insert_non_send_resource(single)
                    .add_system_to_stage(
                        CoreStage::First,
                        run_steam_callbacks.label(SteamworksSystem::RunCallbacks),
                    );

                add_event::<AuthSessionTicketResponse>(app, &client);
                add_event::<DownloadItemResult>(app, &client);
                add_event::<GameLobbyJoinRequested>(app, &client);
                add_event::<LobbyChatUpdate>(app, &client);
                add_event::<P2PSessionConnectFail>(app, &client);
                add_event::<P2PSessionRequest>(app, &client);
                add_event::<PersonaStateChange>(app, &client);
                add_event::<SteamServerConnectFailure>(app, &client);
                add_event::<SteamServersConnected>(app, &client);
                add_event::<SteamServersDisconnected>(app, &client);
                add_event::<UserAchievementStored>(app, &client);
                add_event::<UserStatsReceived>(app, &client);
                add_event::<UserStatsStored>(app, &client);
                add_event::<ValidateAuthTicketResponse>(app, &client);
            }
        }
    }
}

/// A set of [`SystemLabel`]s for systems used by [`SteamworksPlugin`]
///
/// [`SystemLabel`]: bevy_ecs::schedule::SystemLabel
#[derive(Debug, Clone, Copy, Eq, Hash, SystemLabel, PartialEq)]
pub enum SteamworksSystem {
    /// A system that runs the Steam SDK callbacks. Anything dependent on
    /// Steam API results should run after this. This runs in
    /// [`CoreStage::First`].
    RunCallbacks,
    /// A set of systems for flushing events from the Steam SDK into bevy.
    /// If using [`EventReader`] with any of these events, it should be
    /// scheduled after these systems. These systems run in
    /// [`CoreStage::PreUpdate`].
    ///
    /// [`EventReader`]: bevy_ecs::event::EventReader
    FlushEvents,
}

fn run_steam_callbacks(client: NonSend<SingleClient>) {
    client.run_callbacks();
}

fn flush_events<T: Send + Sync + 'static>(
    events: ResMut<SteamEvents<T>>,
    mut output: EventWriter<T>,
) {
    let mut pending = events.pending.lock();
    if !pending.is_empty() {
        output.send_batch(pending.drain(0..));
    }
}

fn add_event<T: Callback + Send + Sync + 'static>(
    app: &mut App,
    client: &steamworks::Client<ClientManager>,
) {
    let pending = Arc::new(Mutex::new(Vec::new()));
    let pending_in = pending.clone();
    app.add_event::<T>()
        .insert_resource(SteamEvents::<T> {
            _callback: client.register_callback::<T, _>(move |evt| {
                pending_in.lock().push(evt);
            }),
            pending,
        })
        .add_system_to_stage(
            CoreStage::PreUpdate,
            flush_events::<T>
                .label(SteamworksSystem::FlushEvents)
                .after(SteamworksSystem::RunCallbacks),
        );
}