Skip to content

Commit

Permalink
Merge pull request #143 from Jackson0ne/master
Browse files Browse the repository at this point in the history
Added multiple Achievement API functions
  • Loading branch information
Noxime authored Nov 28, 2023
2 parents ea0af23 + a08fe0e commit a46bbb6
Show file tree
Hide file tree
Showing 3 changed files with 361 additions and 1 deletion.
146 changes: 146 additions & 0 deletions examples/achievements/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# 🏆 Achievements Example
*By [Jackson0ne](https://github.com/Jackson0ne)*

This example outlines how to use the following achievement functions from the Steamworks API within `steamworks-rs`:

`get_achievement_achieved_percent()`
-

> *⚠ This function **requires** a successful callback to be received from `request_global_achievement_percentages(...)` before it will return any data!*
Returns the percentage of users who have unlocked the specified achievement.

To use, you'll first need to call `client.user_stats().request_global_achievement_percentages(...)`, and obtain the result from within the returned callback.

#### Example:

```rust
use steamworks::{Client,AppId};

fn main() {
let (client,single) = Client::init_app(AppId(4000)).unwrap();
let name = "GMA_BALLEATER";

client.user_stats().request_global_achievement_percentages(move|result| {
if !result.is_err() {
let user_stats = client.user_stats();
let achievement = user_stats.achievement(name);

let ach_percent = achievement.get_achievement_achieved_percent().unwrap();
} else {
eprintln!("Error fetching achievement percentage for {}",name);
}
});

for _ in 0..50 {
single.run_callbacks();
std::thread::sleep(std::time::Duration::from_millis(100));
}
}
```

`get_achievement_display_attribute("name" | "desc" | "hidden")`
-

Returns a string for the result of the specified attribute type. Accepted values are:

- `"name"`: The friendly (*non-API*) name of the achievement
- `"desc"`: The achievement description
- `"hidden"`: Whether the achievement is hidden (`"1"`) or not (`"0"`).

> *As the returned value is always a string, the `"hidden"` value will need to be parsed for comparison!*
#### Example:

```rust
use steamworks::{Client,AppId};

fn main() {
let (client,single) = Client::init_app(AppId(4000)).unwrap();
let name = "GMA_BALLEATER";

let user_stats = client.user_stats();
let achievement = user_stats.achievement(name);

let ach_name = achievement.get_achievement_display_attribute("name").unwrap();
let ach_desc = achievement.get_achievement_display_attribute("desc").unwrap();
let ach_hidden = achievement.get_achievement_display_attribute("hidden").unwrap().parse::<u32>().unwrap();

println!(
"Name: {:?}\nDesc: {:?}\nHidden?: {:?}",
ach_name,
ach_desc,
ach_hidden != 0
);
}
```

`get_achievement_icon()`
-

Returns a `Vec<u8>` buffer containing the image data for the specified achievement.


- The icon is always returned as `64px` x `64px`.
- The version of the icon that is downloaded - i.e. locked (*grey*) vs unlocked (*colour*) - is dependent on whether the achievement has been unlocked or not at the time the function is called.

> *As far as I can tell, there's no parameter to request a specific version!*
To convert the buffer into an image, you can use an external crate to convert the `Vec<u8>` (`Uint8Array`) into a file (such as `.jpg`) and save it to disk - there's plenty of [Rust crates](https://crates.io/crates/image) or [NPM libraries](https://www.npmjs.com/package/jpeg-js) that can do this.

#### Example:

```rust
use steamworks::{Client,AppId};

fn main() {
let (client,single) = Client::init_app(AppId(4000)).unwrap();
let name = "GMA_BALLEATER";

let user_stats = client.user_stats();
let achievement = user_stats.achievement(name);

let _ach_icon_handle = achievement.get_achievement_icon().expect("Failed getting achievement icon RGBA buffer");
}
```

`get_num_achievements()`
-

Returns the number of achievements for the current AppId.

> *Returns `0` if the current AppId has no achievements.*
#### Example:

```rust
use steamworks::{Client,AppId};

fn main() {
let (client,single) = Client::init_app(AppId(4000)).unwrap();

let num = client.user_stats().get_num_achievements().expect("Failed to get number of achievements");

println!("{}",num);
}
```

`get_achievement_names()`
-

Returns a `Vec<String>` containing the API names of all achievements for the current AppId.

> *The returned string value will be empty if the specified index is invalid.*
#### Example:

```rust
use steamworks::{Client,AppId};

fn main() {
let (client,single) = Client::init_app(AppId(4000)).unwrap();
let name = "GMA_BALLEATER";

let names = client.user_stats().get_achievement_names().expect("Failed to get achievement names");
}
```
77 changes: 77 additions & 0 deletions src/user_stats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,40 @@ impl<Manager> UserStats<Manager> {
}
}

/// Asynchronously fetch the data for the percentage of players who have received each achievement
/// for the current game globally.
///
/// You must have called `request_current_stats()` and it needs to return successfully via its
/// callback prior to calling this!*
///
/// **Note: Not sure if this is applicable, as the other achievement functions requiring
/// `request_current_stats()` don't specifically need it to be called in order for them to complete
/// successfully. Maybe it autoruns via `Client::init()/init_app()` somehow?*
pub fn request_global_achievement_percentages<F>(&self, cb: F)
where
F: FnOnce(Result<GameId, SteamError>) + 'static + Send,
{
unsafe {
let api_call = sys::SteamAPI_ISteamUserStats_RequestGlobalAchievementPercentages(
self.user_stats,
);
register_call_result::<sys::GlobalAchievementPercentagesReady_t, _, _>(
&self.inner,
api_call,
// `CALLBACK_BASE_ID + <number>`: <number> is found in Steamworks `isteamuserstats.h` header file
// (Under `struct GlobalAchievementPercentagesReady_t {...};` in this case)
CALLBACK_BASE_ID + 10,
move |v, io_error| {
cb(if io_error {
Err(SteamError::IOFailure)
} else {
Ok(GameId(v.m_nGameID))
})
},
);
}
}

/// Send the changed stats and achievements data to the server for permanent storage.
///
/// * Triggers a [`UserStatsStored`](../struct.UserStatsStored.html) callback if successful.
Expand Down Expand Up @@ -433,6 +467,49 @@ impl<Manager> UserStats<Manager> {
parent: self,
}
}

/// Get the number of achievements defined in the App Admin panel of the Steamworks website.
///
/// This is used for iterating through all of the achievements with GetAchievementName.
///
/// Returns 0 if the current App ID has no achievements.
///
/// *Note: Returns an error for AppId `480` (Spacewar)!*
pub fn get_num_achievements(&self) -> Result<u32,()> {
unsafe {
let num = sys::SteamAPI_ISteamUserStats_GetNumAchievements(
self.user_stats,
);
if num != 0 {
Ok(num)
} else {
Err(())
}
}
}

/// Returns an array of all achievement names for the current AppId.
///
/// Returns an empty string for an achievement name if `iAchievement` is not a valid index,
/// and the current AppId must have achievements.
pub fn get_achievement_names(&self) -> Option<Vec<String>> {
let num = self.get_num_achievements().expect("Failed to get number of achievements");
let mut names = Vec::new();

for i in 0..num {
unsafe {
let name = sys::SteamAPI_ISteamUserStats_GetAchievementName(
self.user_stats,
i
);

let c_str = CStr::from_ptr(name).to_string_lossy().into_owned();

names.push(c_str);
}
}
Some(names)
}
}

#[derive(Clone, Debug)]
Expand Down
139 changes: 138 additions & 1 deletion src/user_stats/stats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,141 @@ impl<M> AchievementHelper<'_, M> {
Err(())
}
}
}

/// Returns the percentage of users who have unlocked the specified achievement.
///
/// You must have called `request_global_achievement_percentages()` and it needs to return
/// successfully via its callback prior to calling this.
///
/// *Note: Always returns an error for AppId `480` (Spacewar)!
/// Other AppIds work fine though.*
///
/// # Example
///
/// ```no_run
/// # use steamworks::*;
/// # let (client, single) = steamworks::Client::init().unwrap();
/// // Get the current unlock percentage for the 'WIN_THE_GAME' achievement
/// client.user_stats().request_global_achievement_percentages(move|result| {
/// if !result.is_err() {
/// let user_stats = client.user_stats();
/// let achievement = user_stats.achievement("WIN_THE_GAME");
/// let ach_percent = achievement.get_achievement_achieved_percent().expect("Failed to get achievement percentage");
///
/// println!("{}",ach_percent);
/// } else {
/// println!("Error requesting global achievement percentages");
/// }
/// });
/// # Err(())
/// ```
pub fn get_achievement_achieved_percent(&self) -> Result<f32, ()> {
unsafe {
let mut percent = 0.0;
let success = sys::SteamAPI_ISteamUserStats_GetAchievementAchievedPercent(
self.parent.user_stats,
self.name.as_ptr() as *const _,
&mut percent as *mut _,
);
if success {
Ok(percent)
} else {
Err(())
}
}
}

/// Get general attributes for an achievement. Currently provides: `Name`, `Description`,
/// and `Hidden` status.
///
/// This receives the value from a dictionary/map keyvalue store, so you must provide one
/// of the following keys:
///
/// - `"name"` to retrive the localized achievement name in UTF8
/// - `"desc"` to retrive the localized achievement description in UTF8
/// - `"hidden"` for retrieving if an achievement is hidden. Returns `"0"` when not hidden,
/// `"1"` when hidden
///
/// This localization is provided based on the games language if it's set, otherwise it
/// checks if a localization is available for the users Steam UI Language. If that fails
/// too, then it falls back to english.
///
/// This function returns the value as a `string` upon success if all of the following
/// conditions are met; otherwise, an empty string: `""`.
///
/// - `request_current_stats()` has completed and successfully returned its callback.
/// - The specified achievement exists in App Admin on the Steamworks website, and the
/// changes are published.
/// - The specified `pchKey` is valid.
///
/// # Example
///
/// ```no_run
/// # use steamworks::*;
/// # let (client, single) = steamworks::Client::init().unwrap();
/// // Get the "description" string for the 'WIN_THE_GAME' achievement
/// client.user_stats().achievement("WIN_THE_GAME").get_achievement_display_attribute("desc").unwrap();
/// # Err(())
/// ```
pub fn get_achievement_display_attribute(&self, key: &str) -> Result<&str, ()> {
unsafe {
let key_c_str = CString::new(key).expect("Failed to create c_str from key parameter");
let ptr = key_c_str.as_ptr() as *const i8;

let str = sys::SteamAPI_ISteamUserStats_GetAchievementDisplayAttribute(
self.parent.user_stats,
self.name.as_ptr() as *const _,
ptr,
);

let c_str = CStr::from_ptr(str);

match c_str.to_str() {
Ok(result) => Ok(result),
Err(_) => Err(()),
}
}
}

/// Gets the icon for an achievement.
///
/// The image is returned as a handle to be used with `ISteamUtils::GetImageRGBA` to get
/// the actual image data.*
///
/// **Note: This is handled within the function. Returns a `Vec<u8>` buffer on success,
/// which can be converted into the image data and saved to disk (e.g. via external RGBA to image crate).*
pub fn get_achievement_icon(&self) -> Option<Vec<u8>> {
unsafe {
let utils = sys::SteamAPI_SteamUtils_v010();
let img = sys::SteamAPI_ISteamUserStats_GetAchievementIcon(
self.parent.user_stats,
self.name.as_ptr() as *const _,
);
if img == 0 {
return None
}
let mut width = 0;
let mut height = 0;
if !sys::SteamAPI_ISteamUtils_GetImageSize(
utils,
img,
&mut width,
&mut height
) {
return None
}
assert_eq!(width, 64);
assert_eq!(height, 64);
let mut dest = vec![0; 64 * 64 * 4];
if !sys::SteamAPI_ISteamUtils_GetImageRGBA(
utils,
img,
dest.as_mut_ptr(),
64 * 64 * 4
) {
return None
}
Some(dest)
}
}
}

0 comments on commit a46bbb6

Please sign in to comment.