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}