Skip to main content

steam_client/services/
rich_presence.rs

1//! Rich Presence functionality for Steam client.
2//!
3//! Rich Presence allows games to display custom status information
4//! that other users can see when viewing a friend's profile or the
5//! friends list.
6
7use std::collections::HashMap;
8
9use steam_enums::EMsg;
10use steamid::SteamID;
11
12use crate::{error::SteamError, SteamClient};
13
14/// Rich Presence data for a user.
15#[derive(Debug, Clone, Default)]
16pub struct RichPresenceData {
17    /// The SteamID of the user.
18    pub steam_id: SteamID,
19    /// The app ID the rich presence is for.
20    pub appid: u32,
21    /// Key-value pairs of rich presence data.
22    pub data: HashMap<String, String>,
23}
24
25impl SteamClient {
26    /// Upload rich presence data for an app.
27    ///
28    /// Rich presence is displayed to friends and on your profile.
29    /// The data is a key-value map of strings.
30    ///
31    /// # Arguments
32    /// * `appid` - The app ID to set rich presence for
33    /// * `data` - Key-value pairs of rich presence data
34    ///
35    /// # Example
36    /// ```rust,ignore
37    /// use std::collections::HashMap;
38    ///
39    /// let mut presence = HashMap::new();
40    /// presence.insert("status".to_string(), "In Main Menu".to_string());
41    /// presence.insert("connect".to_string(), "+connect localhost:27015".to_string());
42    ///
43    /// client.upload_rich_presence(730, &presence).await?;
44    /// ```
45    pub async fn upload_rich_presence(&mut self, appid: u32, data: &HashMap<String, String>) -> Result<(), SteamError> {
46        if !self.is_logged_in() {
47            return Err(SteamError::NotLoggedOn);
48        }
49
50        // Encode rich presence as binary KeyValues
51        let kv_bytes = encode_rich_presence_kv(data);
52
53        // Record for session recovery
54        self.session_recovery.record_rich_presence(appid, data.clone());
55
56        let msg = steam_protos::CMsgClientRichPresenceUpload { rich_presence_kv: Some(kv_bytes), ..Default::default() };
57
58        // Send with routing_appid in header
59        self.send_message_with_routing(EMsg::ClientRichPresenceUpload, appid, &msg).await
60    }
61
62    /// Request rich presence data for users.
63    ///
64    /// # Arguments
65    /// * `appid` - The app ID to get rich presence for
66    /// * `steam_ids` - SteamIDs of users to request rich presence for
67    ///
68    /// # Example
69    /// ```rust,ignore
70    /// let steam_ids = vec![steam_id_1, steam_id_2];
71    /// client.request_rich_presence(730, &steam_ids).await?;
72    /// // Rich presence data will arrive as SteamEvent::RichPresence events
73    /// ```
74    pub async fn request_rich_presence(&mut self, appid: u32, steam_ids: &[SteamID]) -> Result<(), SteamError> {
75        if !self.is_logged_in() {
76            return Err(SteamError::NotLoggedOn);
77        }
78
79        if steam_ids.is_empty() {
80            return Ok(());
81        }
82
83        let msg = steam_protos::CMsgClientRichPresenceRequest { steamid_request: steam_ids.iter().map(|sid| sid.steam_id64()).collect() };
84
85        self.send_message_with_routing(EMsg::ClientRichPresenceRequest, appid, &msg).await
86    }
87
88    /// Get rich presence localization tokens for an app.
89    ///
90    /// # Arguments
91    /// * `appid` - The app ID to get localizations for
92    /// * `language` - The language to get localizations for (e.g., "english")
93    pub async fn get_app_rich_presence_localization(&mut self, appid: i32, language: &str) -> Result<(), SteamError> {
94        if !self.is_logged_in() {
95            return Err(SteamError::NotLoggedOn);
96        }
97
98        let request = steam_protos::CCommunityGetAppRichPresenceLocalizationRequest { appid: Some(appid), language: Some(language.to_string()) };
99
100        self.send_service_method("Community.GetAppRichPresenceLocalization#1", &request).await
101    }
102
103    /// Clear rich presence for an app.
104    ///
105    /// This removes all rich presence data for the specified app.
106    pub async fn clear_rich_presence(&mut self, appid: u32) -> Result<(), SteamError> {
107        self.upload_rich_presence(appid, &HashMap::new()).await
108    }
109}
110
111/// Encode rich presence data as binary KeyValues format.
112///
113/// Format:
114/// - 0x00 byte (start)
115/// - "RP\0" (null-terminated string)
116/// - For each key-value:
117///   - 0x01 (type: string)
118///   - key\0 (null-terminated)
119///   - value\0 (null-terminated)
120/// - 0x08 (end)
121/// - 0x08 (end again)
122fn encode_rich_presence_kv(data: &HashMap<String, String>) -> Vec<u8> {
123    let mut buf = Vec::with_capacity(1024);
124
125    // Start marker
126    buf.push(0x00);
127
128    // "RP" section name (null-terminated)
129    buf.extend_from_slice(b"RP\0");
130
131    // Key-value pairs
132    for (key, value) in data {
133        buf.push(0x01); // type: string
134        buf.extend_from_slice(key.as_bytes());
135        buf.push(0x00); // null terminator
136        buf.extend_from_slice(value.as_bytes());
137        buf.push(0x00); // null terminator
138    }
139
140    // End markers
141    buf.push(0x08);
142    buf.push(0x08);
143
144    buf
145}
146
147/// Parse binary KeyValues rich presence data.
148pub fn parse_rich_presence_kv(data: &[u8]) -> HashMap<String, String> {
149    let mut result = HashMap::new();
150
151    if data.is_empty() {
152        return result;
153    }
154
155    let mut i = 0;
156
157    // Skip start marker
158    if i < data.len() && data[i] == 0x00 {
159        i += 1;
160    }
161
162    // Skip section name (e.g., "RP\0")
163    while i < data.len() && data[i] != 0x00 {
164        i += 1;
165    }
166    if i < data.len() {
167        i += 1; // skip null terminator
168    }
169
170    // Parse key-value pairs
171    while i < data.len() {
172        let type_byte = data[i];
173        i += 1;
174
175        if type_byte == 0x08 {
176            // End marker
177            break;
178        }
179
180        if type_byte != 0x01 {
181            // Not a string type, skip
182            continue;
183        }
184
185        // Read key (null-terminated)
186        let key_start = i;
187        while i < data.len() && data[i] != 0x00 {
188            i += 1;
189        }
190        let key = String::from_utf8_lossy(&data[key_start..i]).to_string();
191        if i < data.len() {
192            i += 1; // skip null
193        }
194
195        // Read value (null-terminated)
196        let value_start = i;
197        while i < data.len() && data[i] != 0x00 {
198            i += 1;
199        }
200        let value = String::from_utf8_lossy(&data[value_start..i]).to_string();
201        if i < data.len() {
202            i += 1; // skip null
203        }
204
205        result.insert(key, value);
206    }
207
208    result
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    #[test]
216    fn test_encode_decode_rich_presence() {
217        let mut data = HashMap::new();
218        data.insert("status".to_string(), "In Menu".to_string());
219        data.insert("connect".to_string(), "+connect 1.2.3.4:27015".to_string());
220
221        let encoded = encode_rich_presence_kv(&data);
222        let decoded = parse_rich_presence_kv(&encoded);
223
224        assert_eq!(decoded.get("status"), Some(&"In Menu".to_string()));
225        assert_eq!(decoded.get("connect"), Some(&"+connect 1.2.3.4:27015".to_string()));
226    }
227
228    #[test]
229    fn test_empty_rich_presence() {
230        let data = HashMap::new();
231        let encoded = encode_rich_presence_kv(&data);
232        let decoded = parse_rich_presence_kv(&encoded);
233
234        assert!(decoded.is_empty());
235    }
236}