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;