aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlex Butler <[email protected]>2019-05-16 11:59:20 +0100
committerMatthew Collins <[email protected]>2019-05-16 17:35:04 +0100
commit7d2ed5ea57f3ea769e659d8c80fd52d60624553e (patch)
tree3156afb74c6f2249bd1beb37cb9761b1c40b723e
parentSkip fields with missing enum types (diff)
downloadsteamworks-rs-7d2ed5ea57f3ea769e659d8c80fd52d60624553e.tar.xz
steamworks-rs-7d2ed5ea57f3ea769e659d8c80fd52d60624553e.zip
Add stats & achievements API
-rw-r--r--src/user_stats.rs33
-rw-r--r--src/user_stats/stat_callback.rs113
-rw-r--r--src/user_stats/stats.rs80
-rwxr-xr-xsteamworks-sys/build.rs21
-rw-r--r--steamworks-sys/src/lib.rs35
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
+}