diff options
| author | Alex Butler <[email protected]> | 2019-05-16 11:59:20 +0100 |
|---|---|---|
| committer | Matthew Collins <[email protected]> | 2019-05-16 17:35:04 +0100 |
| commit | 7d2ed5ea57f3ea769e659d8c80fd52d60624553e (patch) | |
| tree | 3156afb74c6f2249bd1beb37cb9761b1c40b723e | |
| parent | Skip fields with missing enum types (diff) | |
| download | steamworks-rs-7d2ed5ea57f3ea769e659d8c80fd52d60624553e.tar.xz steamworks-rs-7d2ed5ea57f3ea769e659d8c80fd52d60624553e.zip | |
Add stats & achievements API
| -rw-r--r-- | src/user_stats.rs | 33 | ||||
| -rw-r--r-- | src/user_stats/stat_callback.rs | 113 | ||||
| -rw-r--r-- | src/user_stats/stats.rs | 80 | ||||
| -rwxr-xr-x | steamworks-sys/build.rs | 21 | ||||
| -rw-r--r-- | steamworks-sys/src/lib.rs | 35 |
5 files changed, 272 insertions, 10 deletions
diff --git a/src/user_stats.rs b/src/user_stats.rs index 745b76a..2ab976b 100644 --- a/src/user_stats.rs +++ b/src/user_stats.rs @@ -1,4 +1,7 @@ +pub mod stats; +mod stat_callback; +pub use self::stat_callback::*; use super::*; /// Access to the steam user interface @@ -141,6 +144,34 @@ impl <Manager> UserStats<Manager> { }); } } + + /// Triggers a [`UserStatsReceived`](./struct.UserStatsReceived.html) callback. + pub fn request_current_stats(&self) { + unsafe { sys::SteamAPI_ISteamUserStats_RequestCurrentStats(self.user_stats); } + } + + /// Send the changed stats and achievements data to the server for permanent storage. + /// + /// * Triggers a [`UserStatsStored`](../struct.UserStatsStored.html) callback if successful. + /// * Triggers a [`UserAchievementStored`](../struct.UserAchievementStored.html) callback + /// if achievements have been unlocked. + /// + /// Requires [`request_current_stats()`](#method.request_current_stats) to have been called + /// and a successful [`UserStatsReceived`](./struct.UserStatsReceived.html) callback processed. + pub fn store_stats(&self) -> Result<(), ()> { + let success = unsafe { sys::SteamAPI_ISteamUserStats_StoreStats(self.user_stats) }; + if success { Ok(()) } else { Err(()) } + } + + /// Access achievement API for a given achievement 'API Name'. + /// + /// Requires [`request_current_stats()`](#method.request_current_stats) to have been called + /// and a successful [`UserStatsReceived`](./struct.UserStatsReceived.html) callback processed. + #[inline] + #[must_use] + pub fn achievement(&self, name: &str) -> stats::AchievementHelper<'_, Manager> { + stats::AchievementHelper { name: CString::new(name).unwrap(), parent: self } + } } #[derive(Debug)] @@ -211,4 +242,4 @@ fn test() { single.run_callbacks(); ::std::thread::sleep(::std::time::Duration::from_millis(100)); } -}
\ No newline at end of file +} diff --git a/src/user_stats/stat_callback.rs b/src/user_stats/stat_callback.rs new file mode 100644 index 0000000..da62e6a --- /dev/null +++ b/src/user_stats/stat_callback.rs @@ -0,0 +1,113 @@ +use super::*; + +/// Callback type after calling +/// [`request_current_stats()`](struct.UserStats.html#method.request_current_stats). +/// +/// # Example +/// +/// ```no_run +/// # use steamworks::*; +/// # let (client, single) = steamworks::Client::init().unwrap(); +/// let callback_handle = client.register_callback(|val: UserStatsReceived| { +/// if val.result.is_err() { +/// // ... +/// } +/// }); +/// ``` +#[derive(Debug)] +pub struct UserStatsReceived { + pub steam_id: SteamId, + pub game_id: GameId, + pub result: Result<(), SteamError>, +} + +unsafe impl Callback for UserStatsReceived { + const ID: i32 = CALLBACK_BASE_ID + 1; + const SIZE: i32 = std::mem::size_of::<sys::UserStatsReceived_t>() as i32; + + unsafe fn from_raw(raw: *mut libc::c_void) -> Self { + let val = &mut *(raw as *mut sys::UserStatsReceived_t); + Self { + steam_id: SteamId(val.m_steamIDUser.0), + game_id: GameId(val.m_nGameID), + result: match val.m_eResult { + sys::EResult::EResultOK => Ok(()), + err => Err(err.into()), + }, + } + } +} + +/// Callback triggered by [`store()`](stats/struct.StatsHelper.html#method.store). +/// +/// # Example +/// +/// ```no_run +/// # use steamworks::*; +/// # let (client, single) = steamworks::Client::init().unwrap(); +/// let callback_handle = client.register_callback(|val: UserStatsStored| { +/// if val.result.is_err() { +/// // ... +/// } +/// }); +/// ``` +#[derive(Debug)] +pub struct UserStatsStored { + pub game_id: GameId, + pub result: Result<(), SteamError>, +} + +unsafe impl Callback for UserStatsStored { + const ID: i32 = CALLBACK_BASE_ID + 2; + const SIZE: i32 = std::mem::size_of::<sys::UserStatsStored_t>() as i32; + + unsafe fn from_raw(raw: *mut libc::c_void) -> Self { + let val = &mut *(raw as *mut sys::UserStatsStored_t); + Self { + game_id: GameId(val.m_nGameID), + result: match val.m_eResult { + sys::EResult::EResultOK => Ok(()), + err => Err(err.into()), + }, + } + } +} + +/// Result of a request to store the achievements on the server, or an "indicate progress" call. +/// If both `current_progress` and `max_progress` are zero, that means the achievement has been +/// fully unlocked. +/// +/// # Example +/// +/// ```no_run +/// # use steamworks::*; +/// # let (client, single) = steamworks::Client::init().unwrap(); +/// let callback_handle = client.register_callback(|val: UserAchievementStored| { +/// // ... +/// }); +/// ``` +#[derive(Debug)] +pub struct UserAchievementStored { + pub game_id: GameId, + pub achievement_name: String, + /// Current progress towards the achievement. + pub current_progress: u32, + /// The total amount of progress required to unlock. + pub max_progress: u32, +} + +unsafe impl Callback for UserAchievementStored { + const ID: i32 = CALLBACK_BASE_ID + 3; + const SIZE: i32 = std::mem::size_of::<sys::UserAchievementStored_t>() as i32; + + unsafe fn from_raw(raw: *mut libc::c_void) -> Self { + let val = &mut *(raw as *mut sys::UserAchievementStored_t); + let name = CStr::from_ptr(val.m_rgchAchievementName.as_ptr()).to_owned(); + Self { + game_id: GameId(val.m_nGameID), + achievement_name: name.into_string().unwrap(), + current_progress: val.m_nCurProgress, + max_progress: val.m_nMaxProgress, + } + } +} diff --git a/src/user_stats/stats.rs b/src/user_stats/stats.rs new file mode 100644 index 0000000..56d650f --- /dev/null +++ b/src/user_stats/stats.rs @@ -0,0 +1,80 @@ +use super::*; + +/// Achievement API. +/// +/// Methods require +/// [`request_current_stats()`](../struct.UserStats.html#method.request_current_stats) +/// to have been called and a successful [`UserStatsReceived`](../struct.UserStatsReceived.html) +/// callback processed. +/// +/// # Example +/// +/// ```no_run +/// # use steamworks::*; +/// # let (client, single) = steamworks::Client::init().unwrap(); +/// // Unlock the 'WIN_THE_GAME' achievement +/// client.user_stats().achievement("WIN_THE_GAME").set()?; +/// # Err(()) +/// ``` +pub struct AchievementHelper<'parent, M> { + pub(crate) name: CString, + pub(crate) parent: &'parent UserStats<M>, +} + +impl<M> AchievementHelper<'_, M> { + /// Gets the unlock status of the Achievement. + /// + /// This call only modifies Steam's in-memory state so it is quite cheap. To send the unlock + /// status to the server and to trigger the Steam overlay notification you must call + /// [`store_stats()`](../struct.UserStats.html#method.store_stats). + /// + /// Fails if this achievement's 'API Name' is unknown, or unsuccessful + /// [`UserStatsReceived`](../struct.UserStatsReceived.html). + pub fn get(&self) -> Result<bool, ()> { + unsafe { + let mut achieved = false; + let success = sys::SteamAPI_ISteamUserStats_GetAchievement( + self.parent.user_stats, + self.name.as_ptr() as *const _, + &mut achieved as *mut _, + ); + if success { Ok(achieved) } else { Err(()) } + } + } + + /// Unlocks an achievement. + /// + /// This call only modifies Steam's in-memory state so it is quite cheap. To send the unlock + /// status to the server and to trigger the Steam overlay notification you must call + /// [`store_stats()`](../struct.UserStats.html#method.store_stats). + /// + /// Fails if this achievement's 'API Name' is unknown, or unsuccessful + /// [`UserStatsReceived`](../struct.UserStatsReceived.html). + pub fn set(&self) -> Result<(), ()> { + let success = unsafe { + sys::SteamAPI_ISteamUserStats_SetAchievement( + self.parent.user_stats, + self.name.as_ptr() as *const _, + ) + }; + if success { Ok(()) } else { Err(()) } + } + + /// Resets the unlock status of an achievement. + /// + /// This call only modifies Steam's in-memory state so it is quite cheap. To send the unlock + /// status to the server and to trigger the Steam overlay notification you must call + /// [`store_stats()`](../struct.UserStats.html#method.store_stats). + /// + /// Fails if this achievement's 'API Name' is unknown, or unsuccessful + /// [`UserStatsReceived`](../struct.UserStatsReceived.html). + pub fn clear(&self) -> Result<(), ()> { + let success = unsafe { + sys::SteamAPI_ISteamUserStats_ClearAchievement( + self.parent.user_stats, + self.name.as_ptr() as *const _, + ) + }; + if success { Ok(()) } else { Err(()) } + } +} diff --git a/steamworks-sys/build.rs b/steamworks-sys/build.rs index 80bcf75..d48e409 100755 --- a/steamworks-sys/build.rs +++ b/steamworks-sys/build.rs @@ -112,12 +112,12 @@ use libc::*; } }; let fty_rust = if fty == "const char*" || fty == "*const char" || fty == "const char *" { - "*const libc::c_char".into() + "*const c_char".into() } else if fty == "char*" { - "*mut libc::c_char".into() + "*mut c_char".into() } else if fty == "const char **" { - "*mut *const libc::c_char".into() - }else if fty.ends_with("*") { + "*mut *const c_char".into() + } else if fty.ends_with("*") { if fty.starts_with("const") { let trimmed = fty.trim_start_matches("const ").trim_end_matches("*").trim(); format!("*const {}", c_to_rust(trimmed)?.1).into() @@ -126,9 +126,13 @@ use libc::*; format!("*mut {}", c_to_rust(trimmed)?.1).into() } } else if fty.contains("[") { - let num_start = fty.chars().position(|v| v == '[').unwrap_or(0); - let num_end = fty.chars().position(|v| v == ']').unwrap_or(0); - format!("[{}; {}]", c_to_rust(&fty[..num_start].trim())?.1, &fty[num_start + 1 .. num_end]).into() + let open_square = fty.char_indices().find(|ic| ic.1 == '[').unwrap().0; + let close_square = fty.char_indices().find(|ic| ic.1 == ']').unwrap().0; + format!( + "[{}; {}]", + c_to_rust(&fty[..open_square].trim())?.1, + &fty[open_square + 1..close_square], + ).into() } else if fty.contains("(") { eprintln!("Unsupported field type function pointer: {:?}", fty); return None; @@ -152,6 +156,7 @@ use libc::*; "uintp" => "usize".into(), "class CSteamID" => "CSteamID".into(), "class CGameID" => "CGameID".into(), + "char" => "c_char".into(), val if val.contains("class") => return None, val => val.into(), } @@ -244,4 +249,4 @@ pub struct {} {{"#, packing, derive, s.struct_)?; .compile("steamrust"); Ok(()) -}
\ No newline at end of file +} diff --git a/steamworks-sys/src/lib.rs b/steamworks-sys/src/lib.rs index 4f064a5..fa96442 100644 --- a/steamworks-sys/src/lib.rs +++ b/steamworks-sys/src/lib.rs @@ -151,6 +151,39 @@ extern "C" { pub fn SteamAPI_ISteamUserStats_DownloadLeaderboardEntries(instance: *mut ISteamUserStats, leaderboard: SteamLeaderboard_t, data_request: ELeaderboardDataRequest, start: c_int, end: c_int) -> SteamAPICall_t; pub fn SteamAPI_ISteamUserStats_GetDownloadedLeaderboardEntry(instance: *mut ISteamUserStats, entries: SteamLeaderboardEntries_t, index: c_int, entry: *mut LeaderboardEntry_t, details: *mut i32, details_max: c_int) -> u8; + /// https://partner.steamgames.com/doc/api/ISteamUserStats#RequestCurrentStats + /// + /// Returns true if successful + pub fn SteamAPI_ISteamUserStats_RequestCurrentStats( + instance: *mut ISteamUserStats, + ) -> bool; + /// https://partner.steamgames.com/doc/api/ISteamUserStats#GetAchievement + /// + /// Returns true if successful + pub fn SteamAPI_ISteamUserStats_GetAchievement( + instance: *mut ISteamUserStats, + name: *const c_char, + achieved: *mut bool, + ) -> bool; + /// https://partner.steamgames.com/doc/api/ISteamUserStats#SetAchievement + /// + /// Returns true if successful + pub fn SteamAPI_ISteamUserStats_SetAchievement( + instance: *mut ISteamUserStats, + name: *const c_char, + ) -> bool; + /// https://partner.steamgames.com/doc/api/ISteamUserStats#ClearAchievement + /// + /// Returns true if successful + pub fn SteamAPI_ISteamUserStats_ClearAchievement( + instance: *mut ISteamUserStats, + name: *const c_char, + ) -> bool; + /// https://partner.steamgames.com/doc/api/ISteamUserStats#StoreStats + /// + /// Returns true if successful + pub fn SteamAPI_ISteamUserStats_StoreStats(instance: *mut ISteamUserStats) -> bool; + pub fn SteamAPI_ISteamGameServer_LogOnAnonymous(instance: *mut ISteamGameServer); pub fn SteamAPI_ISteamGameServer_SetProduct(instance: *mut ISteamGameServer, product: *const c_char); pub fn SteamAPI_ISteamGameServer_SetGameDescription(instance: *mut ISteamGameServer, description: *const c_char); @@ -160,4 +193,4 @@ extern "C" { pub fn SteamAPI_ISteamGameServer_BeginAuthSession(instance: *mut ISteamGameServer, ticket: *const c_void, ticket_size: *mut u32, steam_id: CSteamID) -> EBeginAuthSessionResult; pub fn SteamAPI_ISteamGameServer_EndAuthSession(instance: *mut ISteamGameServer, steam_id: CSteamID); pub fn SteamAPI_ISteamGameServer_CancelAuthTicket(instance: *mut ISteamGameServer, auth_ticket: HAuthTicket); -}
\ No newline at end of file +} |