Skip to main content

pfx_steam/
live.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::io::{Read, Write};
3use std::panic::{AssertUnwindSafe, catch_unwind};
4use std::sync::{Arc, Mutex, PoisonError};
5
6use steamworks::networking_messages::SessionRequest;
7use steamworks::networking_types::{NetworkingConnectionState, NetworkingIdentity, SendFlags};
8use steamworks::{
9    CallbackResult, ChatMemberStateChange, Client, ComparisonFilter, DistanceFilter, Leaderboard,
10    LeaderboardDataRequest, LeaderboardDisplayType, LeaderboardEntry, LeaderboardScoreUploaded,
11    LeaderboardSortMethod, LobbyId, LobbyKey, LobbyListFilter, LobbyType, NearFilter, NumberFilter,
12    SResult, SteamAPIInitError, SteamError, SteamId, StringFilter, StringFilterKind,
13    UploadScoreMethod,
14};
15
16use crate::board::split_run;
17use crate::{
18    Board, BoardInfo, Call, Change, Compare, DETAILS_MAX, Delivery, Display, Distance, Entry,
19    Error, Event, Lobby, LobbyFilter, LobbyKind, Message, Overlay, PRESENCE_COUNT_MAX, Quota,
20    RUN_FIELDS, Range, Sort, SteamApi, Ugc, Upload, Uploaded, User, api_name, cloud_error,
21    details_len, file_name, fits, lobby_key, lobby_size, lobby_value, message_len, presence_key,
22    presence_value, save_len,
23};
24
25enum Done {
26    Board(Call, SResult<Option<Leaderboard>>),
27    Uploaded(Call, Board, SResult<Option<LeaderboardScoreUploaded>>),
28    Scores(Call, Board, usize, SResult<Vec<LeaderboardEntry>>),
29    Shared(Call, SResult<u64>),
30    Created(Call, SResult<LobbyId>),
31    Lobbies(Call, SResult<Vec<LobbyId>>),
32    Joined(Call, Result<LobbyId, ()>),
33    Request(SessionRequest),
34}
35
36type Completed = Arc<Mutex<Vec<Done>>>;
37
38pub struct Steam {
39    client: Option<Client>,
40    app_id: u32,
41    presence: BTreeSet<String>,
42    done: Completed,
43    calls: u64,
44    boards: BTreeMap<u64, Leaderboard>,
45    requests: BTreeMap<u64, SessionRequest>,
46    peers: BTreeMap<u64, bool>,
47    pending: Vec<Event>,
48    downloads: Vec<(Call, Ugc, u32)>,
49    #[cfg(any(feature = "input", test))]
50    handles: Handles,
51}
52
53#[cfg(any(feature = "input", test))]
54#[derive(Default)]
55struct Handles {
56    up: bool,
57    sets: BTreeMap<String, u64>,
58    digital: BTreeMap<String, u64>,
59    analog: BTreeMap<String, u64>,
60    origins: BTreeMap<i32, steamworks::sys::EInputActionOrigin>,
61}
62
63impl Steam {
64    pub fn init(app_id: u32) -> Result<Self, Error> {
65        if app_id == 0 {
66            return Err(Error::AppId);
67        }
68        match Client::init_app(app_id) {
69            Ok(client) => {
70                let done: Completed = Arc::default();
71                let requests = done.clone();
72                client
73                    .networking_messages()
74                    .session_request_callback(move |request| {
75                        post(&requests, Done::Request(request));
76                    });
77                Ok(Self {
78                    client: Some(client),
79                    app_id,
80                    presence: BTreeSet::new(),
81                    done,
82                    calls: 0,
83                    boards: BTreeMap::new(),
84                    requests: BTreeMap::new(),
85                    peers: BTreeMap::new(),
86                    pending: Vec::new(),
87                    downloads: Vec::new(),
88                    #[cfg(any(feature = "input", test))]
89                    handles: Handles::default(),
90                })
91            }
92            Err(SteamAPIInitError::NoSteamClient(_)) => Err(Error::NotRunning),
93            Err(SteamAPIInitError::FailedGeneric(message))
94            | Err(SteamAPIInitError::VersionMismatch(message)) => {
95                Err(Error::Init(init_message(message)))
96            }
97        }
98    }
99
100    pub fn app_id(&self) -> u32 {
101        self.app_id
102    }
103
104    fn client(&self) -> Result<&Client, Error> {
105        self.client.as_ref().ok_or(Error::Closed)
106    }
107
108    fn call(&mut self) -> Result<(Call, Completed), Error> {
109        self.client()?;
110        self.calls += 1;
111        Ok((Call(self.calls), self.done.clone()))
112    }
113
114    fn board_info(&mut self, client: &Client, board: Leaderboard) -> BoardInfo {
115        let stats = client.user_stats();
116        let info = BoardInfo {
117            board: Board(board.raw()),
118            name: stats.get_leaderboard_name(&board),
119            sort: match stats.get_leaderboard_sort_method(&board) {
120                Some(LeaderboardSortMethod::Ascending) => Sort::Ascending,
121                _ => Sort::Descending,
122            },
123            display: match stats.get_leaderboard_display_type(&board) {
124                Some(LeaderboardDisplayType::TimeSeconds) => Display::Seconds,
125                Some(LeaderboardDisplayType::TimeMilliSeconds) => Display::Milliseconds,
126                _ => Display::Numeric,
127            },
128            entries: u32::try_from(stats.get_leaderboard_entry_count(&board)).unwrap_or(0),
129        };
130        self.boards.insert(board.raw(), board);
131        info
132    }
133
134    #[cfg(any(feature = "input", test))]
135    fn input(&mut self) -> Result<steamworks::Input, Error> {
136        let input = self.client()?.input();
137        if !self.handles.up {
138            if !input.init(true) {
139                return Err(Error::Rejected);
140            }
141            self.handles.up = true;
142        }
143        Ok(input)
144    }
145
146    #[cfg(any(feature = "input", test))]
147    fn set_handle(&mut self, set: &str) -> Result<u64, Error> {
148        api_name(set)?;
149        let input = self.input()?;
150        handle(&mut self.handles.sets, set, |name| {
151            input.get_action_set_handle(name)
152        })
153    }
154
155    #[cfg(any(feature = "input", test))]
156    fn digital_handle(&mut self, action: &str) -> Result<u64, Error> {
157        api_name(action)?;
158        let input = self.input()?;
159        handle(&mut self.handles.digital, action, |name| {
160            input.get_digital_action_handle(name)
161        })
162    }
163
164    #[cfg(any(feature = "input", test))]
165    fn analog_handle(&mut self, action: &str) -> Result<u64, Error> {
166        api_name(action)?;
167        let input = self.input()?;
168        handle(&mut self.handles.analog, action, |name| {
169            input.get_analog_action_handle(name)
170        })
171    }
172
173    #[cfg(any(feature = "input", test))]
174    fn remember(&mut self, origin: steamworks::sys::EInputActionOrigin) -> i32 {
175        let number = origin as i32;
176        self.handles.origins.insert(number, origin);
177        number
178    }
179
180    fn leaderboard(&self, board: Board) -> Result<&Leaderboard, Error> {
181        self.boards.get(&board.0).ok_or(Error::Unknown)
182    }
183
184    fn finish(&mut self, client: &Client, done: Done) -> Option<Event> {
185        Some(match done {
186            Done::Board(call, result) => Event::Board {
187                call,
188                result: match result {
189                    Ok(Some(board)) => Ok(self.board_info(client, board)),
190                    Ok(None) => Err(Error::Unknown),
191                    Err(error) => Err(steam_error(error)),
192                },
193            },
194            Done::Uploaded(call, board, result) => Event::Uploaded {
195                call,
196                board,
197                result: match result {
198                    Ok(Some(uploaded)) => Ok(Uploaded {
199                        score: uploaded.score,
200                        changed: uploaded.was_changed,
201                        rank: rank(uploaded.global_rank_new),
202                        previous_rank: rank(uploaded.global_rank_previous),
203                    }),
204                    Ok(None) => Err(Error::Rejected),
205                    Err(error) => Err(steam_error(error)),
206                },
207            },
208            Done::Scores(call, board, keep, result) => Event::Scores {
209                call,
210                board,
211                result: result
212                    .map(|entries| {
213                        entries
214                            .into_iter()
215                            .map(|entry| {
216                                let (run, details) = split_run(&entry.details, keep);
217                                Entry {
218                                    user: User(entry.user.raw()),
219                                    rank: rank(entry.global_rank),
220                                    score: entry.score,
221                                    details,
222                                    run,
223                                }
224                            })
225                            .collect()
226                    })
227                    .map_err(steam_error),
228            },
229            Done::Shared(call, result) => Event::Shared {
230                call,
231                result: result.map(Ugc).map_err(steam_error),
232            },
233            Done::Created(call, result) => Event::LobbyCreated {
234                call,
235                result: result.map(|lobby| Lobby(lobby.raw())).map_err(steam_error),
236            },
237            Done::Lobbies(call, result) => Event::Lobbies {
238                call,
239                result: result
240                    .map(|lobbies| lobbies.into_iter().map(|id| Lobby(id.raw())).collect())
241                    .map_err(steam_error),
242            },
243            Done::Joined(call, result) => Event::Joined {
244                call,
245                result: result
246                    .map(|lobby| Lobby(lobby.raw()))
247                    .map_err(|()| Error::Rejected),
248            },
249            Done::Request(request) => {
250                let peer = request.remote().steam_id()?.raw();
251                self.requests.insert(peer, request);
252                Event::SessionRequest { peer: User(peer) }
253            }
254        })
255    }
256
257    fn poll_downloads(&mut self, events: &mut Vec<Event>) {
258        if self.downloads.is_empty() {
259            return;
260        }
261        let remote = unsafe { steamworks::sys::SteamAPI_SteamRemoteStorage_v016() };
262        let mut waiting = Vec::new();
263        for (call, ugc, frames) in std::mem::take(&mut self.downloads) {
264            let result = if remote.is_null() {
265                Some(Err(Error::Rejected))
266            } else {
267                read_ugc(remote, ugc)
268            };
269            match result {
270                Some(result) => events.push(Event::UgcDownloaded { call, ugc, result }),
271                None if frames >= UGC_FRAMES => events.push(Event::UgcDownloaded {
272                    call,
273                    ugc,
274                    result: Err(Error::Offline),
275                }),
276                None => waiting.push((call, ugc, frames + 1)),
277            }
278        }
279        self.downloads = waiting;
280    }
281
282    fn poll_peers(&mut self, client: &Client, events: &mut Vec<Event>) {
283        let messages = client.networking_messages();
284        for (peer, connected) in self.peers.iter_mut() {
285            let identity = NetworkingIdentity::new_steam_id(SteamId::from_raw(*peer));
286            let (state, _, _) = messages.get_session_connection_info(&identity);
287            let now = state == NetworkingConnectionState::Connected;
288            if now && !*connected {
289                events.push(Event::Connected { peer: User(*peer) });
290            }
291            *connected = now;
292        }
293    }
294}
295
296#[cfg(any(feature = "input", test))]
297fn handle(
298    cache: &mut BTreeMap<String, u64>,
299    name: &str,
300    find: impl FnOnce(&str) -> u64,
301) -> Result<u64, Error> {
302    if let Some(found) = cache.get(name) {
303        return Ok(*found);
304    }
305    match find(name) {
306        0 => Err(Error::Unknown),
307        found => {
308            cache.insert(name.to_string(), found);
309            Ok(found)
310        }
311    }
312}
313
314#[cfg(any(feature = "input", test))]
315fn input_type(kind: steamworks::InputType) -> i32 {
316    use pfx_input::family as steam;
317    use steamworks::InputType;
318    match kind {
319        InputType::Unknown => steam::STEAM_INPUT_UNKNOWN,
320        InputType::SteamController => steam::STEAM_INPUT_STEAM_CONTROLLER,
321        InputType::XBox360Controller => steam::STEAM_INPUT_XBOX_360,
322        InputType::XBoxOneController => steam::STEAM_INPUT_XBOX_ONE,
323        InputType::GenericGamepad => steam::STEAM_INPUT_GENERIC,
324        InputType::PS4Controller => steam::STEAM_INPUT_PS4,
325        InputType::AppleMFiController => steam::STEAM_INPUT_APPLE_MFI,
326        InputType::AndroidController => steam::STEAM_INPUT_ANDROID,
327        InputType::SwitchJoyConPair => steam::STEAM_INPUT_SWITCH_JOYCON_PAIR,
328        InputType::SwitchJoyConSingle => steam::STEAM_INPUT_SWITCH_JOYCON_SINGLE,
329        InputType::SwitchProController => steam::STEAM_INPUT_SWITCH_PRO,
330        InputType::MobileTouch => steam::STEAM_INPUT_MOBILE_TOUCH,
331        InputType::PS3Controller => steam::STEAM_INPUT_PS3,
332        InputType::PS5Controller => steam::STEAM_INPUT_PS5,
333        InputType::SteamDeckController => steam::STEAM_INPUT_STEAM_DECK,
334    }
335}
336
337const UGC_FRAMES: u32 = 36_000;
338
339fn read_ugc(
340    remote: *mut steamworks::sys::ISteamRemoteStorage,
341    ugc: Ugc,
342) -> Option<Result<Vec<u8>, Error>> {
343    let mut app = 0;
344    let mut name = std::ptr::null_mut();
345    let mut size = 0i32;
346    let mut owner: steamworks::sys::CSteamID = unsafe { std::mem::zeroed() };
347    let ready = unsafe {
348        steamworks::sys::SteamAPI_ISteamRemoteStorage_GetUGCDetails(
349            remote, ugc.0, &mut app, &mut name, &mut size, &mut owner,
350        )
351    };
352    if !ready {
353        return None;
354    }
355    let Ok(len) = usize::try_from(size) else {
356        return Some(Err(Error::Rejected));
357    };
358    if len > crate::BLOB_MAX {
359        return Some(Err(Error::TooBig));
360    }
361    let mut bytes = vec![0u8; len];
362    let read = unsafe {
363        steamworks::sys::SteamAPI_ISteamRemoteStorage_UGCRead(
364            remote,
365            ugc.0,
366            bytes.as_mut_ptr().cast(),
367            size,
368            0,
369            steamworks::sys::EUGCReadAction::k_EUGCRead_ContinueReadingUntilFinished,
370        )
371    };
372    match usize::try_from(read) {
373        Ok(read) if read == len => Some(Ok(bytes)),
374        _ => Some(Err(Error::Rejected)),
375    }
376}
377
378fn post(completed: &Completed, done: Done) {
379    completed
380        .lock()
381        .unwrap_or_else(PoisonError::into_inner)
382        .push(done);
383}
384
385fn steam_error(error: SteamError) -> Error {
386    match error {
387        SteamError::IOFailure | SteamError::Timeout | SteamError::NoConnection => Error::Offline,
388        _ => Error::Rejected,
389    }
390}
391
392fn rank(rank: i32) -> u32 {
393    u32::try_from(rank).unwrap_or(0)
394}
395
396fn compare(compare: Compare) -> ComparisonFilter {
397    match compare {
398        Compare::Equal => ComparisonFilter::Equal,
399        Compare::NotEqual => ComparisonFilter::NotEqual,
400        Compare::Less => ComparisonFilter::LessThan,
401        Compare::LessOrEqual => ComparisonFilter::LessThanEqualTo,
402        Compare::Greater => ComparisonFilter::GreaterThan,
403        Compare::GreaterOrEqual => ComparisonFilter::GreaterThanEqualTo,
404    }
405}
406
407fn string_compare(compare: Compare) -> StringFilterKind {
408    match compare {
409        Compare::Equal => StringFilterKind::Equal,
410        Compare::NotEqual => StringFilterKind::NotEqual,
411        Compare::Less => StringFilterKind::LessThan,
412        Compare::LessOrEqual => StringFilterKind::EqualToOrLessThan,
413        Compare::Greater => StringFilterKind::GreaterThan,
414        Compare::GreaterOrEqual => StringFilterKind::EqualToOrGreaterThan,
415    }
416}
417
418fn key(key: &str) -> Result<LobbyKey<'_>, Error> {
419    LobbyKey::try_new(key).map_err(|_| Error::Rejected)
420}
421
422fn change(change: ChatMemberStateChange) -> Change {
423    match change {
424        ChatMemberStateChange::Entered => Change::Entered,
425        ChatMemberStateChange::Left => Change::Left,
426        ChatMemberStateChange::Disconnected => Change::Disconnected,
427        ChatMemberStateChange::Kicked => Change::Kicked,
428        ChatMemberStateChange::Banned => Change::Banned,
429    }
430}
431
432const UNDECODED_MAX: usize = 64;
433
434fn drain(mut run: impl FnMut(), mut free: impl FnMut()) -> usize {
435    let mut skipped = 0;
436    while skipped < UNDECODED_MAX && catch_unwind(AssertUnwindSafe(&mut run)).is_err() {
437        free();
438        skipped += 1;
439    }
440    skipped
441}
442
443fn free_last_callback() {
444    unsafe {
445        steamworks::sys::SteamAPI_ManualDispatch_FreeLastCallback(
446            steamworks::sys::SteamAPI_GetHSteamPipe(),
447        );
448    }
449}
450
451impl SteamApi for Steam {
452    fn run_callbacks(&mut self) -> Vec<Event> {
453        let Some(client) = self.client.clone() else {
454            return Vec::new();
455        };
456        let mut events = std::mem::take(&mut self.pending);
457        let mut failed = Vec::new();
458        let skipped = drain(
459            || {
460                client.process_callbacks(|callback| match callback {
461                    CallbackResult::GameOverlayActivated(overlay) => {
462                        events.push(Event::Overlay(if overlay.active {
463                            Overlay::Opened
464                        } else {
465                            Overlay::Closed
466                        }));
467                    }
468                    CallbackResult::LobbyChatUpdate(update) => events.push(Event::Member {
469                        lobby: Lobby(update.lobby.raw()),
470                        user: User(update.user_changed.raw()),
471                        change: change(update.member_state_change),
472                    }),
473                    CallbackResult::LobbyDataUpdate(update)
474                        if update.member.raw() == update.lobby.raw() =>
475                    {
476                        events.push(Event::LobbyData {
477                            lobby: Lobby(update.lobby.raw()),
478                        });
479                    }
480                    CallbackResult::GameLobbyJoinRequested(request) => {
481                        events.push(Event::JoinRequested {
482                            lobby: Lobby(request.lobby_steam_id.raw()),
483                            friend: Some(User(request.friend_steam_id.raw())),
484                        });
485                    }
486                    CallbackResult::NetworkingMessagesSessionFailed(failure) => {
487                        if let Some(peer) = failure
488                            .info
489                            .identity_remote()
490                            .and_then(|identity| identity.steam_id())
491                        {
492                            failed.push(Event::SessionFailed {
493                                peer: User(peer.raw()),
494                                reason: failure.info.end_reason().map(i32::from),
495                            });
496                        }
497                    }
498                    _ => {}
499                })
500            },
501            free_last_callback,
502        );
503        events.extend(std::iter::repeat_n(Event::Unrecognised, skipped));
504        let finished =
505            std::mem::take(&mut *self.done.lock().unwrap_or_else(PoisonError::into_inner));
506        for done in finished {
507            if let Some(event) = self.finish(&client, done) {
508                events.push(event);
509            }
510        }
511        for event in &failed {
512            if let Event::SessionFailed { peer, .. } = event {
513                self.peers.insert(peer.0, false);
514            }
515        }
516        events.extend(failed);
517        self.poll_peers(&client, &mut events);
518        self.poll_downloads(&mut events);
519        events
520    }
521
522    fn shutdown(&mut self) {
523        #[cfg(any(feature = "input", test))]
524        if self.handles.up
525            && let Some(client) = self.client.as_ref()
526        {
527            client.input().shutdown();
528        }
529        #[cfg(any(feature = "input", test))]
530        {
531            self.handles = Handles::default();
532        }
533        self.requests.clear();
534        self.pending.clear();
535        self.downloads.clear();
536        self.boards.clear();
537        self.peers.clear();
538        self.done
539            .lock()
540            .unwrap_or_else(PoisonError::into_inner)
541            .clear();
542        self.client.take();
543        self.presence.clear();
544    }
545
546    fn unlock(&mut self, name: &str) -> Result<(), Error> {
547        let client = self.client()?;
548        api_name(name)?;
549        client
550            .user_stats()
551            .achievement(name)
552            .set()
553            .map_err(|_| Error::Unknown)
554    }
555
556    fn achieved(&self, name: &str) -> Result<bool, Error> {
557        let client = self.client()?;
558        api_name(name)?;
559        client
560            .user_stats()
561            .achievement(name)
562            .get()
563            .map_err(|_| Error::Unknown)
564    }
565
566    fn store(&mut self) -> Result<(), Error> {
567        let client = self.client()?;
568        client
569            .user_stats()
570            .store_stats()
571            .map_err(|_| Error::Rejected)
572    }
573
574    #[cfg(any(test, debug_assertions))]
575    fn clear(&mut self, name: &str) -> Result<(), Error> {
576        let client = self.client()?;
577        api_name(name)?;
578        client
579            .user_stats()
580            .achievement(name)
581            .clear()
582            .map_err(|_| Error::Unknown)
583    }
584
585    fn stat_i32(&self, name: &str) -> Result<i32, Error> {
586        let client = self.client()?;
587        api_name(name)?;
588        client
589            .user_stats()
590            .get_stat_i32(name)
591            .map_err(|_| Error::Unknown)
592    }
593
594    fn set_stat_i32(&mut self, name: &str, value: i32) -> Result<(), Error> {
595        let client = self.client()?;
596        api_name(name)?;
597        client
598            .user_stats()
599            .set_stat_i32(name, value)
600            .map_err(|_| Error::Unknown)
601    }
602
603    fn stat_f32(&self, name: &str) -> Result<f32, Error> {
604        let client = self.client()?;
605        api_name(name)?;
606        client
607            .user_stats()
608            .get_stat_f32(name)
609            .map_err(|_| Error::Unknown)
610    }
611
612    fn set_stat_f32(&mut self, name: &str, value: f32) -> Result<(), Error> {
613        let client = self.client()?;
614        api_name(name)?;
615        if !value.is_finite() {
616            return Err(Error::Rejected);
617        }
618        client
619            .user_stats()
620            .set_stat_f32(name, value)
621            .map_err(|_| Error::Unknown)
622    }
623
624    fn write(&mut self, name: &str, bytes: &[u8]) -> Result<(), Error> {
625        let client = self.client()?;
626        let storage = client.remote_storage();
627        cloud_error(
628            storage.is_cloud_enabled_for_account(),
629            storage.is_cloud_enabled_for_app(),
630        )?;
631        file_name(name)?;
632        save_len(bytes.len())?;
633        let old = file_size(&storage, name);
634        let quota = read_quota()?;
635        fits(quota.available, old, bytes.len())?;
636        let mut writer = storage.file(name).write();
637        let wrote = writer.write_all(bytes);
638        drop(writer);
639        wrote.map_err(|_| Error::Rejected)
640    }
641
642    fn read(&self, name: &str) -> Result<Vec<u8>, Error> {
643        let client = self.client()?;
644        file_name(name)?;
645        let storage = client.remote_storage();
646        cloud_error(
647            storage.is_cloud_enabled_for_account(),
648            storage.is_cloud_enabled_for_app(),
649        )?;
650        if !storage.file(name).exists() {
651            return Err(Error::Missing);
652        }
653        let mut reader = storage.file(name).read();
654        let mut bytes = Vec::new();
655        reader
656            .read_to_end(&mut bytes)
657            .map_err(|_| Error::Rejected)?;
658        Ok(bytes)
659    }
660
661    fn exists(&self, name: &str) -> Result<bool, Error> {
662        let client = self.client()?;
663        file_name(name)?;
664        let storage = client.remote_storage();
665        cloud_error(
666            storage.is_cloud_enabled_for_account(),
667            storage.is_cloud_enabled_for_app(),
668        )?;
669        Ok(storage.file(name).exists())
670    }
671
672    fn delete(&mut self, name: &str) -> Result<(), Error> {
673        let client = self.client()?;
674        file_name(name)?;
675        let storage = client.remote_storage();
676        cloud_error(
677            storage.is_cloud_enabled_for_account(),
678            storage.is_cloud_enabled_for_app(),
679        )?;
680        let file = storage.file(name);
681        if !file.exists() {
682            return Err(Error::Missing);
683        }
684        if file.delete() {
685            Ok(())
686        } else {
687            Err(Error::Rejected)
688        }
689    }
690
691    fn quota(&self) -> Result<Quota, Error> {
692        let client = self.client()?;
693        let storage = client.remote_storage();
694        cloud_error(
695            storage.is_cloud_enabled_for_account(),
696            storage.is_cloud_enabled_for_app(),
697        )?;
698        read_quota()
699    }
700
701    fn set_presence(&mut self, key: &str, value: &str) -> Result<(), Error> {
702        presence_key(key)?;
703        presence_value(value)?;
704        let known = self.presence.contains(key);
705        if !known && self.presence.len() >= PRESENCE_COUNT_MAX {
706            return Err(Error::Rejected);
707        }
708        let accepted = self.client()?.friends().set_rich_presence(key, Some(value));
709        if !accepted {
710            return Err(Error::Rejected);
711        }
712        self.presence.insert(key.to_string());
713        Ok(())
714    }
715
716    fn clear_presence(&mut self, key: &str) -> Result<(), Error> {
717        presence_key(key)?;
718        let accepted = self.client()?.friends().set_rich_presence(key, None);
719        if !accepted {
720            return Err(Error::Rejected);
721        }
722        self.presence.remove(key);
723        Ok(())
724    }
725
726    fn me(&self) -> Result<User, Error> {
727        Ok(User(self.client()?.user().steam_id().raw()))
728    }
729
730    fn server_time(&self) -> Result<Option<u64>, Error> {
731        let seconds = self.client()?.utils().get_server_real_time();
732        Ok((seconds != 0).then_some(u64::from(seconds)))
733    }
734
735    fn find_board(&mut self, name: &str) -> Result<Call, Error> {
736        api_name(name)?;
737        let (call, done) = self.call()?;
738        self.client()?
739            .user_stats()
740            .find_leaderboard(name, move |result| post(&done, Done::Board(call, result)));
741        Ok(call)
742    }
743
744    fn find_or_create_board(
745        &mut self,
746        name: &str,
747        sort: Sort,
748        display: Display,
749    ) -> Result<Call, Error> {
750        api_name(name)?;
751        let (call, done) = self.call()?;
752        let sort = match sort {
753            Sort::Ascending => LeaderboardSortMethod::Ascending,
754            Sort::Descending => LeaderboardSortMethod::Descending,
755        };
756        let display = match display {
757            Display::Numeric => LeaderboardDisplayType::Numeric,
758            Display::Seconds => LeaderboardDisplayType::TimeSeconds,
759            Display::Milliseconds => LeaderboardDisplayType::TimeMilliSeconds,
760        };
761        self.client()?.user_stats().find_or_create_leaderboard(
762            name,
763            sort,
764            display,
765            move |result| post(&done, Done::Board(call, result)),
766        );
767        Ok(call)
768    }
769
770    fn upload_score(
771        &mut self,
772        board: Board,
773        score: i32,
774        details: &[i32],
775        upload: Upload,
776    ) -> Result<Call, Error> {
777        details_len(details)?;
778        self.leaderboard(board)?;
779        let (call, done) = self.call()?;
780        let method = match upload {
781            Upload::KeepBest => UploadScoreMethod::KeepBest,
782            Upload::Force => UploadScoreMethod::ForceUpdate,
783        };
784        self.client()?.user_stats().upload_leaderboard_score(
785            self.leaderboard(board)?,
786            method,
787            score,
788            details,
789            move |result| post(&done, Done::Uploaded(call, board, result)),
790        );
791        Ok(call)
792    }
793
794    fn download_scores(
795        &mut self,
796        board: Board,
797        range: Range,
798        details: usize,
799    ) -> Result<Call, Error> {
800        let details = details.min(DETAILS_MAX);
801        if !range.valid() {
802            return Err(Error::Rejected);
803        }
804        self.leaderboard(board)?;
805        let (call, done) = self.call()?;
806        let (request, first, last) = match range {
807            Range::Global { first, last } => (
808                LeaderboardDataRequest::Global,
809                first as usize,
810                last as usize,
811            ),
812            Range::AroundUser { before, after } => (
813                LeaderboardDataRequest::GlobalAroundUser,
814                (before as usize).wrapping_neg(),
815                after as usize,
816            ),
817            Range::Friends => (LeaderboardDataRequest::Friends, 0, 0),
818        };
819        self.client()?.user_stats().download_leaderboard_entries(
820            self.leaderboard(board)?,
821            request,
822            first,
823            last,
824            (details + RUN_FIELDS).min(DETAILS_MAX),
825            move |result| post(&done, Done::Scores(call, board, details, result)),
826        );
827        Ok(call)
828    }
829
830    fn share_file(&mut self, name: &str) -> Result<Call, Error> {
831        file_name(name)?;
832        let storage = self.client()?.remote_storage();
833        cloud_error(
834            storage.is_cloud_enabled_for_account(),
835            storage.is_cloud_enabled_for_app(),
836        )?;
837        if !storage.file(name).exists() {
838            return Err(Error::Missing);
839        }
840        let (call, done) = self.call()?;
841        storage
842            .file(name)
843            .share(move |result| post(&done, Done::Shared(call, result)));
844        Ok(call)
845    }
846
847    fn attach_ugc(&mut self, board: Board, ugc: Ugc) -> Result<(), Error> {
848        self.client()?;
849        let board = self.leaderboard(board)?.raw();
850        let stats = unsafe { steamworks::sys::SteamAPI_SteamUserStats_v013() };
851        if stats.is_null() {
852            return Err(Error::Rejected);
853        }
854        unsafe {
855            steamworks::sys::SteamAPI_ISteamUserStats_AttachLeaderboardUGC(stats, board, ugc.0)
856        };
857        Ok(())
858    }
859
860    fn download_ugc(&mut self, ugc: Ugc) -> Result<Call, Error> {
861        let (call, _) = self.call()?;
862        let remote = unsafe { steamworks::sys::SteamAPI_SteamRemoteStorage_v016() };
863        if remote.is_null() {
864            return Err(Error::Rejected);
865        }
866        unsafe { steamworks::sys::SteamAPI_ISteamRemoteStorage_UGCDownload(remote, ugc.0, 0) };
867        self.downloads.push((call, ugc, 0));
868        Ok(call)
869    }
870
871    fn create_lobby(&mut self, kind: LobbyKind, max_members: u32) -> Result<Call, Error> {
872        lobby_size(max_members)?;
873        let (call, done) = self.call()?;
874        let kind = match kind {
875            LobbyKind::Private => LobbyType::Private,
876            LobbyKind::FriendsOnly => LobbyType::FriendsOnly,
877            LobbyKind::Public => LobbyType::Public,
878            LobbyKind::Invisible => LobbyType::Invisible,
879        };
880        self.client()?
881            .matchmaking()
882            .create_lobby(kind, max_members, move |result| {
883                post(&done, Done::Created(call, result))
884            });
885        Ok(call)
886    }
887
888    fn find_lobbies(&mut self, filter: &LobbyFilter) -> Result<Call, Error> {
889        filter.check()?;
890        let strings = filter
891            .strings
892            .iter()
893            .map(|(name, value, how)| Ok(StringFilter(key(name)?, value, string_compare(*how))))
894            .collect::<Result<Vec<_>, Error>>()?;
895        let numbers = filter
896            .numbers
897            .iter()
898            .map(|(name, value, how)| Ok(NumberFilter(key(name)?, *value, compare(*how))))
899            .collect::<Result<Vec<_>, Error>>()?;
900        let near = filter
901            .near
902            .iter()
903            .map(|(name, value)| Ok(NearFilter(key(name)?, *value)))
904            .collect::<Result<Vec<_>, Error>>()?;
905        let (call, done) = self.call()?;
906        let list = LobbyListFilter {
907            string: (!strings.is_empty()).then_some(strings),
908            number: (!numbers.is_empty()).then_some(numbers),
909            near_value: (!near.is_empty()).then_some(near),
910            open_slots: filter.open_slots,
911            distance: filter.distance.map(|distance| match distance {
912                Distance::Close => DistanceFilter::Close,
913                Distance::Default => DistanceFilter::Default,
914                Distance::Far => DistanceFilter::Far,
915                Distance::Worldwide => DistanceFilter::Worldwide,
916            }),
917            count: filter.count.map(u64::from),
918        };
919        let matchmaking = self.client()?.matchmaking();
920        matchmaking
921            .set_lobby_list_filter(list)
922            .request_lobby_list(move |result| post(&done, Done::Lobbies(call, result)));
923        Ok(call)
924    }
925
926    fn join_lobby(&mut self, lobby: Lobby) -> Result<Call, Error> {
927        let (call, done) = self.call()?;
928        self.client()?
929            .matchmaking()
930            .join_lobby(LobbyId::from_raw(lobby.0), move |result| {
931                post(&done, Done::Joined(call, result))
932            });
933        Ok(call)
934    }
935
936    fn leave_lobby(&mut self, lobby: Lobby) -> Result<(), Error> {
937        self.client()?
938            .matchmaking()
939            .leave_lobby(LobbyId::from_raw(lobby.0));
940        Ok(())
941    }
942
943    fn lobby_members(&self, lobby: Lobby) -> Result<Vec<User>, Error> {
944        Ok(self
945            .client()?
946            .matchmaking()
947            .lobby_members(LobbyId::from_raw(lobby.0))
948            .into_iter()
949            .map(|member| User(member.raw()))
950            .collect())
951    }
952
953    fn lobby_owner(&self, lobby: Lobby) -> Result<User, Error> {
954        let owner = self
955            .client()?
956            .matchmaking()
957            .lobby_owner(LobbyId::from_raw(lobby.0))
958            .raw();
959        if owner == 0 {
960            Err(Error::Rejected)
961        } else {
962            Ok(User(owner))
963        }
964    }
965
966    fn set_lobby_data(&mut self, lobby: Lobby, key: &str, value: &str) -> Result<(), Error> {
967        lobby_key(key)?;
968        lobby_value(value)?;
969        let matchmaking = self.client()?.matchmaking();
970        let lobby = LobbyId::from_raw(lobby.0);
971        let accepted = if value.is_empty() {
972            matchmaking.delete_lobby_data(lobby, key)
973        } else {
974            matchmaking.set_lobby_data(lobby, key, value)
975        };
976        if accepted {
977            Ok(())
978        } else {
979            Err(Error::Rejected)
980        }
981    }
982
983    fn lobby_data(&self, lobby: Lobby, key: &str) -> Result<Option<String>, Error> {
984        lobby_key(key)?;
985        Ok(self
986            .client()?
987            .matchmaking()
988            .lobby_data(LobbyId::from_raw(lobby.0), key)
989            .filter(|value| !value.is_empty()))
990    }
991
992    fn invite_dialog(&mut self, lobby: Lobby) -> Result<(), Error> {
993        self.client()?
994            .friends()
995            .activate_invite_dialog(LobbyId::from_raw(lobby.0));
996        Ok(())
997    }
998
999    fn launched(&mut self, args: &[String]) -> Option<Lobby> {
1000        let lobby = crate::connect_lobby(args)?;
1001        self.client().ok()?;
1002        self.pending.push(Event::JoinRequested {
1003            lobby,
1004            friend: None,
1005        });
1006        Some(lobby)
1007    }
1008
1009    fn send_message(
1010        &mut self,
1011        peer: User,
1012        channel: u32,
1013        bytes: &[u8],
1014        delivery: Delivery,
1015    ) -> Result<(), Error> {
1016        message_len(bytes.len())?;
1017        if peer == self.me()? {
1018            return Err(Error::Rejected);
1019        }
1020        let flags = match delivery {
1021            Delivery::Reliable => SendFlags::RELIABLE,
1022            Delivery::Unreliable => SendFlags::UNRELIABLE,
1023        } | SendFlags::AUTO_RESTART_BROKEN_SESSION;
1024        let identity = NetworkingIdentity::new_steam_id(SteamId::from_raw(peer.0));
1025        self.client()?
1026            .networking_messages()
1027            .send_message_to_user(identity, flags, bytes, channel)
1028            .map_err(steam_error)?;
1029        self.peers.entry(peer.0).or_insert(false);
1030        Ok(())
1031    }
1032
1033    fn receive_messages(&mut self, channel: u32, max: usize) -> Result<Vec<Message>, Error> {
1034        if max == 0 {
1035            return Ok(Vec::new());
1036        }
1037        Ok(self
1038            .client()?
1039            .networking_messages()
1040            .receive_messages_on_channel(channel, max)
1041            .into_iter()
1042            .filter_map(|message| {
1043                let peer = message.identity_peer().steam_id()?;
1044                Some(Message {
1045                    peer: User(peer.raw()),
1046                    channel,
1047                    bytes: message.data().to_vec(),
1048                })
1049            })
1050            .collect())
1051    }
1052
1053    fn accept_session(&mut self, peer: User) -> Result<(), Error> {
1054        self.client()?;
1055        let request = self.requests.remove(&peer.0).ok_or(Error::Missing)?;
1056        if request.accept() {
1057            self.peers.entry(peer.0).or_insert(false);
1058            Ok(())
1059        } else {
1060            Err(Error::Rejected)
1061        }
1062    }
1063
1064    #[cfg(any(feature = "input", test))]
1065    fn input_frame(&mut self) -> Result<Vec<u64>, Error> {
1066        let input = self.input()?;
1067        input.run_frame();
1068        Ok(input
1069            .get_connected_controllers()
1070            .into_iter()
1071            .filter(|handle| *handle != 0)
1072            .collect())
1073    }
1074
1075    #[cfg(any(feature = "input", test))]
1076    fn input_type(&mut self, pad: u64) -> Result<i32, Error> {
1077        Ok(input_type(self.input()?.get_input_type_for_handle(pad)))
1078    }
1079
1080    #[cfg(any(feature = "input", test))]
1081    fn activate_action_set(&mut self, pad: u64, set: &str) -> Result<(), Error> {
1082        let set = self.set_handle(set)?;
1083        self.input()?.activate_action_set_handle(pad, set);
1084        Ok(())
1085    }
1086
1087    #[cfg(any(feature = "input", test))]
1088    fn digital_action(&mut self, pad: u64, action: &str) -> Result<crate::Digital, Error> {
1089        let action = self.digital_handle(action)?;
1090        let data = self.input()?.get_digital_action_data(pad, action);
1091        Ok(crate::Digital {
1092            state: data.bState,
1093            active: data.bActive,
1094        })
1095    }
1096
1097    #[cfg(any(feature = "input", test))]
1098    fn analog_action(&mut self, pad: u64, action: &str) -> Result<crate::Analog, Error> {
1099        let action = self.analog_handle(action)?;
1100        let data = self.input()?.get_analog_action_data(pad, action);
1101        Ok(crate::Analog {
1102            x: data.x,
1103            y: data.y,
1104            active: data.bActive,
1105        })
1106    }
1107
1108    #[cfg(any(feature = "input", test))]
1109    fn digital_origins(&mut self, pad: u64, set: &str, action: &str) -> Result<Vec<i32>, Error> {
1110        let set = self.set_handle(set)?;
1111        let action = self.digital_handle(action)?;
1112        Ok(self
1113            .input()?
1114            .get_digital_action_origins(pad, set, action)
1115            .into_iter()
1116            .map(|origin| self.remember(origin))
1117            .collect())
1118    }
1119
1120    #[cfg(any(feature = "input", test))]
1121    fn analog_origins(&mut self, pad: u64, set: &str, action: &str) -> Result<Vec<i32>, Error> {
1122        let set = self.set_handle(set)?;
1123        let action = self.analog_handle(action)?;
1124        Ok(self
1125            .input()?
1126            .get_analog_action_origins(pad, set, action)
1127            .into_iter()
1128            .map(|origin| self.remember(origin))
1129            .collect())
1130    }
1131
1132    #[cfg(any(feature = "input", test))]
1133    fn trigger_vibration(&mut self, pad: u64, low: u16, high: u16) -> Result<(), Error> {
1134        self.input()?;
1135        let input = unsafe { steamworks::sys::SteamAPI_SteamInput_v006() };
1136        if input.is_null() {
1137            return Err(Error::Rejected);
1138        }
1139        unsafe { steamworks::sys::SteamAPI_ISteamInput_TriggerVibration(input, pad, low, high) };
1140        Ok(())
1141    }
1142
1143    #[cfg(any(feature = "input", test))]
1144    fn steam_glyph(
1145        &mut self,
1146        origin: i32,
1147        glyph: crate::SteamGlyph,
1148    ) -> Result<Option<String>, Error> {
1149        use steamworks::sys::ESteamInputGlyphSize;
1150        self.input()?;
1151        let Some(known) = self.handles.origins.get(&origin).copied() else {
1152            return Ok(None);
1153        };
1154        let input = unsafe { steamworks::sys::SteamAPI_SteamInput_v006() };
1155        if input.is_null() {
1156            return Err(Error::Rejected);
1157        }
1158        let path = match glyph.format {
1159            crate::GlyphFormat::Png(size) => {
1160                let size = match size {
1161                    crate::GlyphSize::Small => ESteamInputGlyphSize::k_ESteamInputGlyphSize_Small,
1162                    crate::GlyphSize::Medium => ESteamInputGlyphSize::k_ESteamInputGlyphSize_Medium,
1163                    crate::GlyphSize::Large => ESteamInputGlyphSize::k_ESteamInputGlyphSize_Large,
1164                };
1165                unsafe {
1166                    steamworks::sys::SteamAPI_ISteamInput_GetGlyphPNGForActionOrigin(
1167                        input,
1168                        known,
1169                        size,
1170                        glyph.flags(),
1171                    )
1172                }
1173            }
1174            crate::GlyphFormat::Svg => unsafe {
1175                steamworks::sys::SteamAPI_ISteamInput_GetGlyphSVGForActionOrigin(
1176                    input,
1177                    known,
1178                    glyph.flags(),
1179                )
1180            },
1181        };
1182        if path.is_null() {
1183            return Ok(None);
1184        }
1185        let path = unsafe { std::ffi::CStr::from_ptr(path) }
1186            .to_string_lossy()
1187            .into_owned();
1188        Ok((!path.is_empty()).then_some(path))
1189    }
1190}
1191
1192fn init_message(message: String) -> String {
1193    if message.is_empty() {
1194        "Steam failed to start".to_string()
1195    } else {
1196        message
1197    }
1198}
1199
1200fn file_size(storage: &steamworks::RemoteStorage, name: &str) -> u64 {
1201    storage
1202        .files()
1203        .into_iter()
1204        .find(|file| file.name == name)
1205        .map(|file| file.size)
1206        .unwrap_or(0)
1207}
1208
1209fn read_quota() -> Result<Quota, Error> {
1210    let remote = unsafe { steamworks::sys::SteamAPI_SteamRemoteStorage_v016() };
1211    if remote.is_null() {
1212        return Err(Error::Rejected);
1213    }
1214    let mut total = 0u64;
1215    let mut available = 0u64;
1216    let ok = unsafe {
1217        steamworks::sys::SteamAPI_ISteamRemoteStorage_GetQuota(remote, &mut total, &mut available)
1218    };
1219    if ok {
1220        Ok(Quota { total, available })
1221    } else {
1222        Err(Error::Rejected)
1223    }
1224}
1225
1226#[cfg(test)]
1227mod tests {
1228    use super::*;
1229
1230    #[test]
1231    fn every_member_state_steam_names_converts() {
1232        for (state, expected) in [
1233            (ChatMemberStateChange::Entered, Change::Entered),
1234            (ChatMemberStateChange::Left, Change::Left),
1235            (ChatMemberStateChange::Disconnected, Change::Disconnected),
1236            (ChatMemberStateChange::Kicked, Change::Kicked),
1237            (ChatMemberStateChange::Banned, Change::Banned),
1238        ] {
1239            assert_eq!(change(state), expected);
1240        }
1241    }
1242
1243    #[test]
1244    fn a_callback_the_crate_cannot_decode_is_skipped_and_freed_not_fatal() {
1245        let mut runs = 0;
1246        let mut freed = 0;
1247        let skipped = drain(
1248            || {
1249                runs += 1;
1250                if runs <= 2 {
1251                    let state = 0x40_u32;
1252                    match state {
1253                        0x1 => {}
1254                        _ => unreachable!(),
1255                    }
1256                }
1257            },
1258            || freed += 1,
1259        );
1260        assert_eq!((skipped, runs, freed), (2, 3, 2));
1261    }
1262
1263    #[test]
1264    fn a_pass_with_nothing_to_skip_runs_once_and_frees_nothing() {
1265        let mut runs = 0;
1266        let mut freed = 0;
1267        assert_eq!(drain(|| runs += 1, || freed += 1), 0);
1268        assert_eq!((runs, freed), (1, 0));
1269    }
1270
1271    #[test]
1272    fn a_callback_that_never_decodes_stops_at_the_cap() {
1273        let mut freed = 0;
1274        let skipped = drain(|| panic!("undecodable"), || freed += 1);
1275        assert_eq!(skipped, UNDECODED_MAX);
1276        assert_eq!(freed, UNDECODED_MAX);
1277    }
1278}