1use std::collections::HashMap;
4
5use prost::Message;
6use steam_enums::{ECsgoGCMsg, EMsg, EResult};
7use steamid::SteamID;
8
9use crate::{error::SteamError, services::gc::GCProtoHeader, SteamClient};
10
11pub const APP_ID: u32 = 730;
13
14pub struct CSGOClient<'a> {
17 pub(crate) client: &'a mut SteamClient,
18}
19
20impl<'a> CSGOClient<'a> {
21 pub fn new(client: &'a mut SteamClient) -> Self {
22 Self { client }
23 }
24
25 pub async fn send_hello(&mut self) -> Result<(), SteamError> {
29 let msg = steam_protos::CMsgGccStrike15V2MatchmakingClient2GcHello {};
30
31 self.client.send_to_gc_proto(APP_ID, ECsgoGCMsg::MatchmakingClient2GCHello as u32, &msg.encode_to_vec(), GCProtoHeader::default()).await
32 }
33
34 pub async fn get_player_profile(&mut self, steam_id: SteamID) -> Result<Vec<crate::CsgoClientHello>, SteamError> {
40 use crate::services::gc::GCJobResponse;
41
42 let rx = self.client.gc_jobs.create_job_with_timeout(APP_ID, ECsgoGCMsg::PlayersProfile as u32, std::time::Duration::from_secs(5));
44
45 self.client.request_players_profile(steam_id).await?;
47
48 let response = rx.await.map_err(|_| SteamError::ResponseTimeout)?;
50
51 match response {
52 GCJobResponse::Success(payload) => {
53 let profile_msg = steam_protos::CMsgGccStrike15V2PlayersProfile::decode(&payload[..]).map_err(|e| SteamError::bad_response(format!("Failed to decode PlayersProfile: {}", e)))?;
54
55 Ok(profile_msg.account_profiles.iter().map(crate::client::MessageHandler::build_csgo_client_hello).collect())
56 }
57 GCJobResponse::Timeout => Err(SteamError::ResponseTimeout),
58 }
59 }
60
61 pub async fn party_search(&mut self, prime: bool, game_type: u32) -> Result<Vec<crate::CsgoPartyEntry>, SteamError> {
66 use crate::services::gc::GCJobResponse;
67
68 let msg = steam_protos::CMsgGccStrike15V2PartySearch {
69 ver: Some(1), apr: Some(if prime { 1 } else { 0 }),
71 ark: Some(0),
72 grps: vec![],
73 launcher: Some(0),
74 game_type: Some(game_type),
75 };
76
77 let rx = self.client.gc_jobs.create_job(APP_ID, ECsgoGCMsg::Party_Search as u32);
80
81 self.client.send_to_gc_proto(APP_ID, ECsgoGCMsg::Party_Search as u32, &msg.encode_to_vec(), GCProtoHeader::default()).await?;
83
84 let response = rx.await.map_err(|_| SteamError::ResponseTimeout)?;
86
87 match response {
88 GCJobResponse::Success(payload) => {
89 let results = steam_protos::CMsgGccStrike15V2PartySearchResults::decode(&payload[..]).map_err(|e| SteamError::bad_response(format!("Failed to decode PartySearchResults: {}", e)))?;
91
92 Ok(results
93 .entries
94 .into_iter()
95 .map(|e| crate::CsgoPartyEntry {
96 account_id: e.accountid.unwrap_or(0),
97 lobby_id: e.id.unwrap_or(0),
98 game_type: e.game_type.unwrap_or(0),
99 loc: e.loc.unwrap_or(0),
100 })
101 .collect())
102 }
103 GCJobResponse::Timeout => Err(SteamError::ResponseTimeout),
104 }
105 }
106
107 pub async fn create_lobby(&mut self, max_members: i32, lobby_type: i32) -> Result<u64, SteamError> {
109 let msg = steam_protos::CMsgClientMmsCreateLobby {
110 app_id: Some(APP_ID),
111 max_members: Some(max_members),
112 lobby_type: Some(lobby_type),
113 lobby_flags: Some(0),
114 cell_id: self.client.account.read().cell_id,
115 ..Default::default()
116 };
117
118 let response: steam_protos::CMsgClientMmsCreateLobbyResponse = self.client.send_request_and_wait(EMsg::ClientMMSCreateLobby, &msg).await?;
119
120 let eresult = EResult::from_i32(response.eresult.unwrap_or(2)).unwrap_or(EResult::Fail);
121 if eresult != EResult::OK {
122 return Err(SteamError::SteamResult(eresult));
123 }
124
125 Ok(response.steam_id_lobby.unwrap_or(0))
126 }
127
128 pub async fn invite_to_lobby(&mut self, lobby_id: u64, user_id: SteamID) -> Result<(), SteamError> {
130 let msg = steam_protos::CMsgClientMmsInviteToLobby { app_id: Some(APP_ID), steam_id_lobby: Some(lobby_id), steam_id_user_invited: Some(user_id.steam_id64()) };
131
132 self.client.send_message(EMsg::ClientMMSInviteToLobby, &msg).await
133 }
134
135 pub async fn set_rich_presence(&mut self, rp: &CsgoRichPresence) -> Result<(), SteamError> {
140 self.client.upload_rich_presence(APP_ID, &rp.to_map()).await
141 }
142
143 pub async fn join_lobby(&mut self, lobby_id: u64) -> Result<JoinLobbyResult, SteamError> {
147 let persona_name = self.client.account.read().info.as_ref().map(|a| a.name.clone()).unwrap_or_default();
148
149 let msg = steam_protos::CMsgClientMmsJoinLobby { app_id: Some(APP_ID), steam_id_lobby: Some(lobby_id), persona_name: Some(persona_name) };
150
151 let response: steam_protos::CMsgClientMmsJoinLobbyResponse = self.client.send_request_and_wait(EMsg::ClientMMSJoinLobby, &msg).await?;
152
153 Ok(JoinLobbyResult {
154 lobby_id: response.steam_id_lobby.unwrap_or(0),
155 owner_id: response.steam_id_owner.unwrap_or(0),
156 chat_room_enter_response: response.chat_room_enter_response.unwrap_or(0),
157 max_members: response.max_members.unwrap_or(0),
158 lobby_type: response.lobby_type.unwrap_or(0),
159 lobby_flags: response.lobby_flags.unwrap_or(0),
160 members: response
161 .members
162 .into_iter()
163 .map(|m| LobbyMember {
164 steam_id: m.steam_id.unwrap_or(0),
165 persona_name: m.persona_name.unwrap_or_default(),
166 metadata: m.metadata.unwrap_or_default(),
167 })
168 .collect(),
169 metadata: response.metadata,
170 })
171 }
172
173 pub async fn leave_lobby(&mut self, lobby_id: u64) -> Result<(), SteamError> {
175 let msg = steam_protos::CMsgClientMmsLeaveLobby { app_id: Some(APP_ID), steam_id_lobby: Some(lobby_id) };
176
177 let response: steam_protos::CMsgClientMmsLeaveLobbyResponse = self.client.send_request_and_wait(EMsg::ClientMMSLeaveLobby, &msg).await?;
178
179 let eresult = EResult::from_i32(response.eresult.unwrap_or(2)).unwrap_or(EResult::Fail);
180 if eresult != EResult::OK {
181 return Err(SteamError::SteamResult(eresult));
182 }
183
184 Ok(())
185 }
186
187 pub async fn update_lobby(&mut self, lobby_id: u64, config: &LobbyConfig) -> Result<u64, SteamError> {
191 let msg = steam_protos::CMsgClientMmsSetLobbyData {
192 app_id: Some(APP_ID),
193 steam_id_lobby: Some(lobby_id),
194 steam_id_member: Some(0),
195 max_members: config.max_members,
196 lobby_type: config.lobby_type,
197 lobby_flags: config.lobby_flags,
198 metadata: config.metadata.clone(),
199 };
200
201 let response: steam_protos::CMsgClientMmsSetLobbyDataResponse = self.client.send_request_and_wait(EMsg::ClientMMSSetLobbyData, &msg).await?;
202
203 let eresult = EResult::from_i32(response.eresult.unwrap_or(2)).unwrap_or(EResult::Fail);
204 if eresult != EResult::OK {
205 return Err(SteamError::SteamResult(eresult));
206 }
207
208 Ok(response.steam_id_lobby.unwrap_or(0))
209 }
210
211 pub async fn get_lobby_data(&mut self, lobby_id: u64) -> Result<LobbyData, SteamError> {
215 let msg = steam_protos::CMsgClientMmsGetLobbyData { app_id: Some(APP_ID), steam_id_lobby: Some(lobby_id) };
216
217 let response: steam_protos::CMsgClientMmsLobbyData = self.client.send_request_and_wait(EMsg::ClientMMSGetLobbyData, &msg).await?;
218
219 Ok(LobbyData {
220 lobby_id: response.steam_id_lobby.unwrap_or(0),
221 owner_id: response.steam_id_owner.unwrap_or(0),
222 num_members: response.num_members.unwrap_or(0),
223 max_members: response.max_members.unwrap_or(0),
224 lobby_type: response.lobby_type.unwrap_or(0),
225 lobby_flags: response.lobby_flags.unwrap_or(0),
226 members: response
227 .members
228 .into_iter()
229 .map(|m| LobbyMember {
230 steam_id: m.steam_id.unwrap_or(0),
231 persona_name: m.persona_name.unwrap_or_default(),
232 metadata: m.metadata.unwrap_or_default(),
233 })
234 .collect(),
235 metadata: response.metadata,
236 })
237 }
238
239 pub async fn create_and_invite(&mut self, config: &LobbyConfig, users: &[SteamID]) -> Result<u64, SteamError> {
244 let lobby_id = self.create_lobby(config.max_members.unwrap_or(10), config.lobby_type.unwrap_or(1)).await?;
246
247 if config.metadata.is_some() || config.lobby_flags.is_some() {
249 self.update_lobby(lobby_id, config).await?;
250 }
251
252 for user in users {
254 self.invite_to_lobby(lobby_id, *user).await?;
255 }
256
257 Ok(lobby_id)
258 }
259
260 pub async fn party_register(&mut self, prime: bool, game_type: u32) -> Result<(), SteamError> {
264 let msg = steam_protos::CMsgGccStrike15V2PartyRegister {
265 id: Some(0),
266 ver: Some(13960), apr: Some(if prime { 1 } else { 0 }),
268 ark: Some(if prime { 180 } else { 0 }),
269 nby: Some(0),
270 grp: Some(0),
271 slots: Some(0),
272 launcher: Some(0),
273 game_type: Some(game_type),
274 };
275
276 let res = self.client.send_to_gc_proto(APP_ID, ECsgoGCMsg::Party_Register as u32, &msg.encode_to_vec(), GCProtoHeader::default()).await;
277
278 if res.is_ok() {
279 self.client.last_time_party_register = Some(chrono::Utc::now().timestamp_millis());
280 }
281
282 res
283 }
284
285 pub async fn acknowledge_penalty(&mut self) -> Result<(), SteamError> {
290 let msg = steam_protos::CMsgGccStrike15V2AcknowledgePenalty { acknowledged: Some(1) };
291
292 self.client.send_to_gc_proto(APP_ID, ECsgoGCMsg::AcknowledgePenalty as u32, &msg.encode_to_vec(), GCProtoHeader::default()).await
293 }
294}
295
296#[derive(Debug, Clone)]
298pub struct JoinLobbyResult {
299 pub lobby_id: u64,
301 pub owner_id: u64,
303 pub chat_room_enter_response: i32,
305 pub max_members: i32,
307 pub lobby_type: i32,
309 pub lobby_flags: i32,
311 pub members: Vec<LobbyMember>,
313 pub metadata: Option<Vec<u8>>,
315}
316
317#[derive(Debug, Clone)]
319pub struct LobbyMember {
320 pub steam_id: u64,
322 pub persona_name: String,
324 pub metadata: Vec<u8>,
326}
327
328#[derive(Debug, Clone, Default)]
330pub struct LobbyConfig {
331 pub max_members: Option<i32>,
333 pub lobby_type: Option<i32>,
335 pub lobby_flags: Option<i32>,
337 pub metadata: Option<Vec<u8>>,
339}
340
341impl LobbyConfig {
342 pub fn new() -> Self {
344 Self::default()
345 }
346
347 pub fn max_members(mut self, max: i32) -> Self {
349 self.max_members = Some(max);
350 self
351 }
352
353 pub fn lobby_type(mut self, t: i32) -> Self {
355 self.lobby_type = Some(t);
356 self
357 }
358
359 pub fn lobby_flags(mut self, flags: i32) -> Self {
361 self.lobby_flags = Some(flags);
362 self
363 }
364
365 pub fn metadata(mut self, data: Vec<u8>) -> Self {
367 self.metadata = Some(data);
368 self
369 }
370}
371
372#[derive(Debug, Clone)]
374pub struct LobbyData {
375 pub lobby_id: u64,
377 pub owner_id: u64,
379 pub num_members: i32,
381 pub max_members: i32,
383 pub lobby_type: i32,
385 pub lobby_flags: i32,
387 pub members: Vec<LobbyMember>,
389 pub metadata: Option<Vec<u8>>,
391}
392
393impl LobbyData {
394 pub fn parse_metadata(&self) -> Option<LobbyMetadata> {
396 self.metadata.as_ref().and_then(|data| LobbyMetadata::decode(data).ok())
397 }
398}
399
400impl JoinLobbyResult {
401 pub fn parse_metadata(&self) -> Option<LobbyMetadata> {
403 self.metadata.as_ref().and_then(|data| LobbyMetadata::decode(data).ok())
404 }
405}
406
407mod kv_type {
413 pub const NONE: u8 = 0; pub const STRING: u8 = 1; pub const END: u8 = 8; }
417
418#[derive(Debug, Clone, Default)]
422pub struct LobbyMetadata {
423 pub rank: Option<String>,
425 pub location: Option<String>,
427 pub map_group_name: Option<String>,
429 pub game_mode: Option<String>,
431 pub prime: Option<String>,
433 pub game_type: Option<String>,
435 pub num_players: Option<String>,
437 pub action: Option<String>,
439 pub any_type_mode: Option<String>,
441 pub access: Option<String>,
443 pub network: Option<String>,
445 pub uids: Vec<u32>,
447 pub extra: HashMap<String, String>,
449}
450
451impl LobbyMetadata {
452 pub fn new() -> Self {
454 Self::default()
455 }
456
457 pub fn map_group(mut self, name: impl Into<String>) -> Self {
459 self.map_group_name = Some(name.into());
460 self
461 }
462
463 pub fn mode(mut self, mode: impl Into<String>) -> Self {
465 self.game_mode = Some(mode.into());
466 self
467 }
468
469 pub fn prime(mut self, is_prime: bool) -> Self {
471 self.prime = Some(if is_prime { "1".to_string() } else { "0".to_string() });
472 self
473 }
474
475 pub fn game_type(mut self, t: impl Into<String>) -> Self {
477 self.game_type = Some(t.into());
478 self
479 }
480
481 pub fn num_players(mut self, count: u32) -> Self {
483 self.num_players = Some(count.to_string());
484 self
485 }
486
487 pub fn action(mut self, action: impl Into<String>) -> Self {
489 self.action = Some(action.into());
490 self
491 }
492
493 pub fn access(mut self, access: impl Into<String>) -> Self {
495 self.access = Some(access.into());
496 self
497 }
498
499 pub fn network(mut self, network: impl Into<String>) -> Self {
501 self.network = Some(network.into());
502 self
503 }
504
505 pub fn uids(mut self, ids: Vec<u32>) -> Self {
507 self.uids = ids;
508 self
509 }
510
511 pub fn encode(&self) -> Vec<u8> {
520 let mut buf = Vec::new();
521
522 buf.push(0x00);
524 buf.push(0x00);
525
526 if let Some(v) = &self.rank {
528 Self::encode_string(&mut buf, "game:ark", v);
529 }
530 if let Some(v) = &self.location {
531 Self::encode_string(&mut buf, "game:loc", v);
532 }
533 if let Some(v) = &self.map_group_name {
534 Self::encode_string(&mut buf, "game:mapgroupname", v);
535 }
536 if let Some(v) = &self.game_mode {
537 Self::encode_string(&mut buf, "game:mode", v);
538 }
539 if let Some(v) = &self.prime {
540 Self::encode_string(&mut buf, "game:prime", v);
541 }
542 if let Some(v) = &self.game_type {
543 Self::encode_string(&mut buf, "game:type", v);
544 }
545 if let Some(v) = &self.num_players {
546 Self::encode_string(&mut buf, "members:numPlayers", v);
547 }
548 if let Some(v) = &self.action {
549 Self::encode_string(&mut buf, "options:action", v);
550 }
551 if let Some(v) = &self.any_type_mode {
552 Self::encode_string(&mut buf, "options:anytypemode", v);
553 }
554 if let Some(v) = &self.access {
555 Self::encode_string(&mut buf, "system:access", v);
556 }
557 if let Some(v) = &self.network {
558 Self::encode_string(&mut buf, "system:network", v);
559 }
560
561 for (k, v) in &self.extra {
563 Self::encode_string(&mut buf, k, v);
564 }
565
566 if !self.uids.is_empty() {
568 buf.push(kv_type::STRING);
569 Self::write_cstring(&mut buf, "uids");
570 let uid_bytes = Self::encode_uids(&self.uids);
572 buf.extend_from_slice(&uid_bytes);
573 }
574
575 buf.push(0x08);
577
578 buf.push(kv_type::END);
580
581 buf
582 }
583
584 pub fn decode(data: &[u8]) -> Result<Self, crate::error::SteamError> {
586 let mut meta = LobbyMetadata::default();
587 let mut pos = 0;
588
589 if data.len() >= 2 {
591 pos = 2;
592 }
593
594 while pos < data.len() {
595 let type_byte = data[pos];
596 pos += 1;
597
598 match type_byte {
599 kv_type::STRING => {
600 let key = Self::read_cstring(data, &mut pos)?;
602
603 if key == "uids" {
605 let uids = Self::decode_uids(data, &mut pos)?;
607 meta.uids = uids;
608 } else {
609 let value = Self::read_cstring(data, &mut pos)?;
611 meta.set_field(&key, value);
612 }
613 }
614 kv_type::NONE => {
615 let _name = Self::read_cstring(data, &mut pos)?;
617 while pos < data.len() && data[pos] != kv_type::END {
619 pos += 1;
620 }
621 pos += 1; }
623 kv_type::END => {
624 break;
625 }
626 _ => {
627 continue;
629 }
630 }
631 }
632
633 Ok(meta)
634 }
635
636 fn set_field(&mut self, key: &str, value: String) {
637 match key {
638 "game:ark" => self.rank = Some(value),
639 "game:loc" => self.location = Some(value),
640 "game:mapgroupname" => self.map_group_name = Some(value),
641 "game:mode" => self.game_mode = Some(value),
642 "game:prime" => self.prime = Some(value),
643 "game:type" => self.game_type = Some(value),
644 "members:numPlayers" => self.num_players = Some(value),
645 "options:action" => self.action = Some(value),
646 "options:anytypemode" => self.any_type_mode = Some(value),
647 "system:access" => self.access = Some(value),
648 "system:network" => self.network = Some(value),
649 _ => {
650 self.extra.insert(key.to_string(), value);
651 }
652 }
653 }
654
655 fn encode_string(buf: &mut Vec<u8>, key: &str, value: &str) {
656 buf.push(kv_type::STRING);
657 Self::write_cstring(buf, key);
658 Self::write_cstring(buf, value);
659 }
660
661 fn write_cstring(buf: &mut Vec<u8>, s: &str) {
662 buf.extend_from_slice(s.as_bytes());
663 buf.push(0x00); }
665
666 fn read_cstring(data: &[u8], pos: &mut usize) -> Result<String, crate::error::SteamError> {
667 let start = *pos;
668 while *pos < data.len() && data[*pos] != 0x00 {
669 *pos += 1;
670 }
671 let s = String::from_utf8_lossy(&data[start..*pos]).to_string();
672 *pos += 1; Ok(s)
674 }
675
676 fn encode_uids(uids: &[u32]) -> Vec<u8> {
678 let mut buf = Vec::new();
679 for &id in uids {
680 let mut val = id;
681 while val > 0x7f {
682 buf.push(((val | 0x80) & 0xff) as u8);
683 val >>= 7;
684 }
685 buf.push(val as u8);
686 }
687 buf.push(0x00); buf
689 }
690
691 fn decode_uids(data: &[u8], pos: &mut usize) -> Result<Vec<u32>, crate::error::SteamError> {
693 let mut uids = Vec::new();
694 let mut current: u32 = 0;
695 let mut shift = 0;
696
697 while *pos < data.len() {
698 let byte = data[*pos];
699 *pos += 1;
700
701 if byte == 0x00 {
702 if current > 0 || shift > 0 {
704 uids.push(current);
705 }
706 break;
707 }
708
709 current |= ((byte & 0x7f) as u32) << shift;
710 shift += 7;
711
712 if byte & 0x80 == 0 {
713 uids.push(current);
715 current = 0;
716 shift = 0;
717 }
718 }
719
720 Ok(uids)
721 }
722}
723
724#[derive(Debug, Clone, Copy, PartialEq, Eq)]
726pub enum CsgoRank {
727 Unranked = 0,
728 SilverI = 1,
729 SilverII = 2,
730 SilverIII = 3,
731 SilverIV = 4,
732 SilverElite = 5,
733 SilverEliteMaster = 6,
734 GoldNovaI = 7,
735 GoldNovaII = 8,
736 GoldNovaIII = 9,
737 GoldNovaMaster = 10,
738 MasterGuardianI = 11,
739 MasterGuardianII = 12,
740 MasterGuardianElite = 13,
741 DistinguishedMasterGuardian = 14,
742 LegendaryEagle = 15,
743 LegendaryEagleMaster = 16,
744 SupremeMasterFirstClass = 17,
745 TheGlobalElite = 18,
746}
747
748#[derive(Debug, Clone, Copy, PartialEq, Eq)]
750pub enum CsgoGameMode {
751 Casual,
752 Competitive,
753 Wingman,
754 Deathmatch,
755 ArmsRace,
756 Demolition,
757 FlyingScoutsman,
758 DangerZone,
759}
760
761impl CsgoGameMode {
762 fn as_str(&self) -> &'static str {
763 match self {
764 CsgoGameMode::Casual => "casual",
765 CsgoGameMode::Competitive => "competitive",
766 CsgoGameMode::Wingman => "scrimcomp2v2",
767 CsgoGameMode::Deathmatch => "deathmatch",
768 CsgoGameMode::ArmsRace => "gungameprogressive",
769 CsgoGameMode::Demolition => "gungametrbomb",
770 CsgoGameMode::FlyingScoutsman => "flyingscoutsman",
771 CsgoGameMode::DangerZone => "survival",
772 }
773 }
774}
775
776#[derive(Debug, Clone, Default)]
778pub struct CsgoRichPresence {
779 rank: Option<CsgoRank>,
780 wins: Option<u32>,
781 level: Option<u32>,
782 score: Option<u32>,
783 #[allow(dead_code)]
784 leaderboard_time: Option<u32>,
785 status: Option<String>,
786 game_mode: Option<CsgoGameMode>,
787 map_group: Option<String>,
788}
789
790impl CsgoRichPresence {
791 pub fn new() -> Self {
792 Self::default()
793 }
794
795 pub fn rank(mut self, rank: CsgoRank) -> Self {
796 self.rank = Some(rank);
797 self
798 }
799
800 pub fn wins(mut self, wins: u32) -> Self {
801 self.wins = Some(wins);
802 self
803 }
804
805 pub fn level(mut self, level: u32) -> Self {
806 self.level = Some(level);
807 self
808 }
809
810 pub fn score(mut self, score: u32) -> Self {
811 self.score = Some(score);
812 self
813 }
814
815 pub fn status(mut self, status: impl Into<String>) -> Self {
816 self.status = Some(status.into());
817 self
818 }
819
820 pub fn game_mode(mut self, mode: CsgoGameMode) -> Self {
821 self.game_mode = Some(mode);
822 self
823 }
824
825 pub fn map_group(mut self, group: impl Into<String>) -> Self {
826 self.map_group = Some(group.into());
827 self
828 }
829
830 pub fn to_map(&self) -> HashMap<String, String> {
831 let mut map = HashMap::new();
832
833 map.insert("version".to_string(), "1".to_string());
834
835 if let Some(rank) = self.rank {
836 map.insert("competitive_ranking".to_string(), (rank as u32).to_string());
837 }
838
839 if let Some(wins) = self.wins {
840 map.insert("competitive_wins".to_string(), wins.to_string());
841 }
842
843 if let Some(level) = self.level {
844 map.insert("level".to_string(), level.to_string());
845 }
846
847 if let Some(score) = self.score {
848 map.insert("score".to_string(), score.to_string());
849 }
850
851 if let Some(status) = &self.status {
852 map.insert("game:state".to_string(), status.clone());
853 }
854
855 if let Some(mode) = self.game_mode {
856 map.insert("game:mode".to_string(), mode.as_str().to_string());
857 }
858
859 if let Some(group) = &self.map_group {
860 map.insert("game:mapgroupname".to_string(), group.clone());
861 }
862
863 if self.rank.is_some() {
865 map.insert("steam_display".to_string(), "#RP_Status_Competitive".to_string());
866 } else if self.status.is_some() {
867 map.insert("steam_display".to_string(), "#RP_Status_Command".to_string());
869 }
870
871 map
872 }
873}