Skip to main content

pfx_steam/
lib.rs

1mod blob;
2mod board;
3mod daily;
4mod fake;
5mod hash;
6mod health;
7mod hub;
8#[cfg(feature = "live")]
9mod live;
10mod lobby;
11mod lockstep;
12mod net;
13#[cfg(any(feature = "input", test))]
14mod steam_input;
15
16pub use blob::{BLOB_CHUNK, BLOB_MAX, BLOB_OPEN_MAX, Blob, Blobs, send_blob};
17pub use board::{
18    Board, BoardInfo, DETAILS_MAX, Display, Entry, RUN_FIELDS, RUN_MARK, Range, Sort, Ugc, Upload,
19    Uploaded, run_details, split_run,
20};
21pub use daily::{Clock, Daily, Date, Repeat, Today, daily_seed};
22pub use fake::{FAKE_USER, Fake};
23pub use health::{Ending, Health, Lost, Quit, RECONNECT_MS, connect_lobby};
24pub use hub::Hub;
25#[cfg(feature = "live")]
26pub use live::Steam;
27pub use lobby::{
28    Change, Compare, Distance, LOBBY_CODE_KEY, LOBBY_CODE_LEN, LOBBY_KEY_MAX, LOBBY_MEMBERS_MAX,
29    LOBBY_RESULTS, LOBBY_VALUE_MAX, Lobby, LobbyFilter, LobbyKind, find_lobby_by_code, lobby_code,
30    normal_code, set_lobby_code,
31};
32pub use lockstep::{Desync, LOCKSTEP_WINDOW, Lockstep, Turn, checksum};
33pub use net::{Delivery, MESSAGE_MAX, Message};
34#[cfg(any(feature = "input", test))]
35pub use steam_input::{
36    ActionKind, Analog, Digital, GlyphFormat, GlyphSize, GlyphStyle, OriginGlyph, STEAM_PAD_BASE,
37    SteamBeside, SteamFeed, SteamGlyph, SteamPads, origin_control, origin_controls,
38};
39
40pub const STEAMWORKS_VERSION: &str = "0.13.1";
41pub const SDK_VERSION: &str = "1.64";
42pub const REDISTRIBUTABLE_LINUX: &str = "libsteam_api.so";
43pub const REDISTRIBUTABLE_WINDOWS: &str = "steam_api64.dll";
44pub const MAX_SAVE: usize = 1024 * 1024;
45pub const NAME_MAX: usize = 127;
46pub const PRESENCE_KEY_MAX: usize = 63;
47pub const PRESENCE_VALUE_MAX: usize = 255;
48pub const PRESENCE_COUNT_MAX: usize = 30;
49
50#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51pub enum Overlay {
52    Opened,
53    Closed,
54}
55
56#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
57pub struct User(pub u64);
58
59#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
60pub struct Call(pub u64);
61
62#[derive(Clone, Debug, PartialEq, Eq)]
63pub enum Event {
64    Overlay(Overlay),
65    Board {
66        call: Call,
67        result: Result<BoardInfo, Error>,
68    },
69    Uploaded {
70        call: Call,
71        board: Board,
72        result: Result<Uploaded, Error>,
73    },
74    Scores {
75        call: Call,
76        board: Board,
77        result: Result<Vec<Entry>, Error>,
78    },
79    LobbyCreated {
80        call: Call,
81        result: Result<Lobby, Error>,
82    },
83    Lobbies {
84        call: Call,
85        result: Result<Vec<Lobby>, Error>,
86    },
87    Joined {
88        call: Call,
89        result: Result<Lobby, Error>,
90    },
91    Member {
92        lobby: Lobby,
93        user: User,
94        change: Change,
95    },
96    LobbyData {
97        lobby: Lobby,
98    },
99    SessionRequest {
100        peer: User,
101    },
102    Connected {
103        peer: User,
104    },
105    SessionFailed {
106        peer: User,
107        reason: Option<i32>,
108    },
109    JoinRequested {
110        lobby: Lobby,
111        friend: Option<User>,
112    },
113    ConnectionLost {
114        peer: User,
115        lost: Lost,
116    },
117    Reconnected {
118        peer: User,
119    },
120    Forfeit {
121        peer: User,
122        lost: Lost,
123    },
124    PeerQuit {
125        peer: User,
126        quit: Quit,
127    },
128    Shared {
129        call: Call,
130        result: Result<Ugc, Error>,
131    },
132    UgcDownloaded {
133        call: Call,
134        ugc: Ugc,
135        result: Result<Vec<u8>, Error>,
136    },
137    Unrecognised,
138}
139
140#[derive(Clone, Copy, Debug, PartialEq, Eq)]
141pub struct Quota {
142    pub total: u64,
143    pub available: u64,
144}
145
146#[derive(Clone, Copy, Debug, PartialEq, Eq)]
147pub enum CloudOff {
148    Account,
149    App,
150    AccountAndApp,
151}
152
153#[derive(Clone, Debug, PartialEq, Eq)]
154pub enum Error {
155    NotRunning,
156    Init(String),
157    AppId,
158    Closed,
159    CloudOff(CloudOff),
160    TooBig,
161    Quota,
162    Missing,
163    Rejected,
164    Unknown,
165    Offline,
166}
167
168impl std::fmt::Display for Error {
169    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170        match self {
171            Error::NotRunning => write!(f, "Steam is not running"),
172            Error::Init(message) => write!(f, "Steam failed to start: {message}"),
173            Error::AppId => write!(f, "the app id comes from the game's config"),
174            Error::Closed => write!(f, "Steam is shut down"),
175            Error::CloudOff(CloudOff::Account) => {
176                write!(f, "Steam Cloud is off for this account")
177            }
178            Error::CloudOff(CloudOff::App) => write!(f, "Steam Cloud is off for this app"),
179            Error::CloudOff(CloudOff::AccountAndApp) => {
180                write!(f, "Steam Cloud is off for this account and this app")
181            }
182            Error::TooBig => write!(f, "a save is larger than 1 MiB"),
183            Error::Quota => write!(f, "Steam Cloud does not have room for that save"),
184            Error::Missing => write!(f, "file is not in Steam Cloud"),
185            Error::Rejected => write!(f, "Steam rejected the call"),
186            Error::Unknown => write!(f, "Steam does not know that name"),
187            Error::Offline => write!(f, "Steam's servers did not answer"),
188        }
189    }
190}
191
192impl std::error::Error for Error {}
193
194pub trait SteamApi {
195    fn run_callbacks(&mut self) -> Vec<Event>;
196    fn shutdown(&mut self);
197
198    fn unlock(&mut self, name: &str) -> Result<(), Error>;
199    fn achieved(&self, name: &str) -> Result<bool, Error>;
200    fn store(&mut self) -> Result<(), Error>;
201
202    fn sync(&mut self, achieved: &[&str]) -> Result<(), Error> {
203        for name in achieved {
204            api_name(name)?;
205        }
206        let mut grant = false;
207        for name in achieved {
208            if !self.achieved(name)? {
209                self.unlock(name)?;
210                grant = true;
211            }
212        }
213        if grant {
214            self.store()?;
215        }
216        Ok(())
217    }
218
219    #[cfg(any(test, debug_assertions))]
220    fn clear(&mut self, name: &str) -> Result<(), Error>;
221
222    fn stat_i32(&self, name: &str) -> Result<i32, Error>;
223    fn set_stat_i32(&mut self, name: &str, value: i32) -> Result<(), Error>;
224    fn stat_f32(&self, name: &str) -> Result<f32, Error>;
225    fn set_stat_f32(&mut self, name: &str, value: f32) -> Result<(), Error>;
226
227    fn write(&mut self, name: &str, bytes: &[u8]) -> Result<(), Error>;
228    fn read(&self, name: &str) -> Result<Vec<u8>, Error>;
229    fn exists(&self, name: &str) -> Result<bool, Error>;
230    fn delete(&mut self, name: &str) -> Result<(), Error>;
231    fn quota(&self) -> Result<Quota, Error>;
232
233    fn set_presence(&mut self, key: &str, value: &str) -> Result<(), Error>;
234    fn clear_presence(&mut self, key: &str) -> Result<(), Error>;
235
236    fn me(&self) -> Result<User, Error>;
237    fn server_time(&self) -> Result<Option<u64>, Error>;
238
239    fn find_board(&mut self, name: &str) -> Result<Call, Error>;
240    fn find_or_create_board(
241        &mut self,
242        name: &str,
243        sort: Sort,
244        display: Display,
245    ) -> Result<Call, Error>;
246    fn upload_score(
247        &mut self,
248        board: Board,
249        score: i32,
250        details: &[i32],
251        upload: Upload,
252    ) -> Result<Call, Error>;
253    fn download_scores(
254        &mut self,
255        board: Board,
256        range: Range,
257        details: usize,
258    ) -> Result<Call, Error>;
259
260    fn upload_run(
261        &mut self,
262        board: Board,
263        score: i32,
264        details: &[i32],
265        upload: Upload,
266        run: Ugc,
267    ) -> Result<Call, Error> {
268        if details.len() + RUN_FIELDS > DETAILS_MAX {
269            return Err(Error::Rejected);
270        }
271        self.upload_score(board, score, &run_details(run, details), upload)
272    }
273
274    fn share_file(&mut self, name: &str) -> Result<Call, Error>;
275    fn attach_ugc(&mut self, board: Board, ugc: Ugc) -> Result<(), Error>;
276    fn download_ugc(&mut self, ugc: Ugc) -> Result<Call, Error>;
277
278    fn create_lobby(&mut self, kind: LobbyKind, max_members: u32) -> Result<Call, Error>;
279    fn find_lobbies(&mut self, filter: &LobbyFilter) -> Result<Call, Error>;
280    fn join_lobby(&mut self, lobby: Lobby) -> Result<Call, Error>;
281    fn leave_lobby(&mut self, lobby: Lobby) -> Result<(), Error>;
282    fn lobby_members(&self, lobby: Lobby) -> Result<Vec<User>, Error>;
283    fn lobby_owner(&self, lobby: Lobby) -> Result<User, Error>;
284    fn set_lobby_data(&mut self, lobby: Lobby, key: &str, value: &str) -> Result<(), Error>;
285    fn lobby_data(&self, lobby: Lobby, key: &str) -> Result<Option<String>, Error>;
286    fn invite_dialog(&mut self, lobby: Lobby) -> Result<(), Error>;
287    fn launched(&mut self, args: &[String]) -> Option<Lobby>;
288
289    fn send_message(
290        &mut self,
291        peer: User,
292        channel: u32,
293        bytes: &[u8],
294        delivery: Delivery,
295    ) -> Result<(), Error>;
296    fn receive_messages(&mut self, channel: u32, max: usize) -> Result<Vec<Message>, Error>;
297    fn accept_session(&mut self, peer: User) -> Result<(), Error>;
298
299    #[cfg(any(feature = "input", test))]
300    fn input_frame(&mut self) -> Result<Vec<u64>, Error>;
301    #[cfg(any(feature = "input", test))]
302    fn input_type(&mut self, pad: u64) -> Result<i32, Error>;
303    #[cfg(any(feature = "input", test))]
304    fn activate_action_set(&mut self, pad: u64, set: &str) -> Result<(), Error>;
305    #[cfg(any(feature = "input", test))]
306    fn digital_action(&mut self, pad: u64, action: &str) -> Result<Digital, Error>;
307    #[cfg(any(feature = "input", test))]
308    fn analog_action(&mut self, pad: u64, action: &str) -> Result<Analog, Error>;
309    #[cfg(any(feature = "input", test))]
310    fn digital_origins(&mut self, pad: u64, set: &str, action: &str) -> Result<Vec<i32>, Error>;
311    #[cfg(any(feature = "input", test))]
312    fn analog_origins(&mut self, pad: u64, set: &str, action: &str) -> Result<Vec<i32>, Error>;
313    #[cfg(any(feature = "input", test))]
314    fn trigger_vibration(&mut self, pad: u64, low: u16, high: u16) -> Result<(), Error>;
315    #[cfg(any(feature = "input", test))]
316    fn steam_glyph(&mut self, origin: i32, glyph: SteamGlyph) -> Result<Option<String>, Error>;
317}
318
319pub(crate) fn api_name(name: &str) -> Result<(), Error> {
320    if name.is_empty() || name.len() > NAME_MAX || name.as_bytes().contains(&0) {
321        Err(Error::Rejected)
322    } else {
323        Ok(())
324    }
325}
326
327pub(crate) fn file_name(name: &str) -> Result<(), Error> {
328    if name.is_empty() || name.as_bytes().contains(&0) {
329        Err(Error::Rejected)
330    } else {
331        Ok(())
332    }
333}
334
335pub(crate) fn save_len(len: usize) -> Result<(), Error> {
336    if len > MAX_SAVE {
337        Err(Error::TooBig)
338    } else {
339        Ok(())
340    }
341}
342
343pub(crate) fn fits(available: u64, old: u64, new_len: usize) -> Result<(), Error> {
344    let new = u64::try_from(new_len).unwrap_or(u64::MAX);
345    if new > available.saturating_add(old) {
346        Err(Error::Quota)
347    } else {
348        Ok(())
349    }
350}
351
352pub(crate) fn cloud_error(account: bool, app: bool) -> Result<(), Error> {
353    match (account, app) {
354        (true, true) => Ok(()),
355        (false, false) => Err(Error::CloudOff(CloudOff::AccountAndApp)),
356        (false, true) => Err(Error::CloudOff(CloudOff::Account)),
357        (true, false) => Err(Error::CloudOff(CloudOff::App)),
358    }
359}
360
361pub(crate) fn presence_key(key: &str) -> Result<(), Error> {
362    if key.is_empty() || key.len() > PRESENCE_KEY_MAX || key.as_bytes().contains(&0) {
363        Err(Error::Rejected)
364    } else {
365        Ok(())
366    }
367}
368
369pub(crate) fn presence_value(value: &str) -> Result<(), Error> {
370    if value.is_empty() || value.len() > PRESENCE_VALUE_MAX || value.as_bytes().contains(&0) {
371        Err(Error::Rejected)
372    } else {
373        Ok(())
374    }
375}
376
377pub(crate) fn details_len(details: &[i32]) -> Result<(), Error> {
378    if details.len() > DETAILS_MAX {
379        Err(Error::Rejected)
380    } else {
381        Ok(())
382    }
383}
384
385pub(crate) fn lobby_key(key: &str) -> Result<(), Error> {
386    if key.is_empty() || key.len() > LOBBY_KEY_MAX || key.as_bytes().contains(&0) {
387        Err(Error::Rejected)
388    } else {
389        Ok(())
390    }
391}
392
393pub(crate) fn lobby_value(value: &str) -> Result<(), Error> {
394    if value.len() > LOBBY_VALUE_MAX || value.as_bytes().contains(&0) {
395        Err(Error::Rejected)
396    } else {
397        Ok(())
398    }
399}
400
401pub(crate) fn lobby_size(max_members: u32) -> Result<(), Error> {
402    if max_members == 0 || max_members > LOBBY_MEMBERS_MAX {
403        Err(Error::Rejected)
404    } else {
405        Ok(())
406    }
407}
408
409pub(crate) fn message_len(len: usize) -> Result<(), Error> {
410    if len > MESSAGE_MAX {
411        Err(Error::Rejected)
412    } else {
413        Ok(())
414    }
415}
416
417#[cfg(test)]
418mod tests;
419#[cfg(test)]
420mod tests_blob;
421#[cfg(test)]
422mod tests_board;
423#[cfg(test)]
424mod tests_daily;
425#[cfg(test)]
426mod tests_health;
427#[cfg(test)]
428mod tests_invite;
429#[cfg(test)]
430mod tests_lobby;
431#[cfg(test)]
432mod tests_lockstep;
433#[cfg(test)]
434mod tests_net;
435#[cfg(test)]
436mod tests_runs;
437#[cfg(test)]
438mod tests_steam_input;