1#![deny(missing_docs)]
2
3use 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};
72pub 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#[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                            unsafe {
137                                (&mut *pending_in.get()).push(SteamworksEvent::$event_name(evt));
138                            }
139                        })
140                    }),+
141                ],
142                pending,
143            }
144        }
145    };
146}
147
148#[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
167pub struct SteamworksPlugin {
169    steam: Mutex<Option<(steamworks::Client, steamworks::SingleClient)>>,
170}
171
172impl SteamworksPlugin {
173    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    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#[derive(Debug, Clone, Copy, Eq, Hash, SystemSet, PartialEq)]
236pub enum SteamworksSystem {
237    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    let pending = unsafe { &mut *events.pending.get() };
253    if !pending.is_empty() {
254        output.send_batch(pending.drain(0..));
255    }
256}