Skip to main content

pfx_steam/
lobby.rs

1use std::cmp::Ordering;
2use std::collections::BTreeMap;
3
4use crate::hash::draw;
5use crate::{Call, Error, SteamApi, lobby_key, lobby_value};
6
7pub const LOBBY_KEY_MAX: usize = 255;
8pub const LOBBY_VALUE_MAX: usize = 8192;
9pub const LOBBY_MEMBERS_MAX: u32 = 250;
10pub const LOBBY_RESULTS: u32 = 50;
11pub const LOBBY_CODE_KEY: &str = "code";
12pub const LOBBY_CODE_LEN: usize = 6;
13const CODE_LETTERS: &[u8] = b"ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
16pub struct Lobby(pub u64);
17
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub enum LobbyKind {
20    Private,
21    FriendsOnly,
22    Public,
23    Invisible,
24}
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27pub enum Change {
28    Entered,
29    Left,
30    Disconnected,
31    Kicked,
32    Banned,
33}
34
35#[derive(Clone, Copy, Debug, PartialEq, Eq)]
36pub enum Compare {
37    Equal,
38    NotEqual,
39    Less,
40    LessOrEqual,
41    Greater,
42    GreaterOrEqual,
43}
44
45#[derive(Clone, Copy, Debug, PartialEq, Eq)]
46pub enum Distance {
47    Close,
48    Default,
49    Far,
50    Worldwide,
51}
52
53#[derive(Clone, Debug, Default, PartialEq, Eq)]
54pub struct LobbyFilter {
55    pub strings: Vec<(String, String, Compare)>,
56    pub numbers: Vec<(String, i32, Compare)>,
57    pub near: Vec<(String, i32)>,
58    pub open_slots: Option<u8>,
59    pub distance: Option<Distance>,
60    pub count: Option<u32>,
61}
62
63impl Compare {
64    pub fn holds(self, order: Ordering) -> bool {
65        match self {
66            Compare::Equal => order == Ordering::Equal,
67            Compare::NotEqual => order != Ordering::Equal,
68            Compare::Less => order == Ordering::Less,
69            Compare::LessOrEqual => order != Ordering::Greater,
70            Compare::Greater => order == Ordering::Greater,
71            Compare::GreaterOrEqual => order != Ordering::Less,
72        }
73    }
74}
75
76impl LobbyFilter {
77    pub fn string(mut self, key: &str, value: &str, compare: Compare) -> Self {
78        self.strings
79            .push((key.to_string(), value.to_string(), compare));
80        self
81    }
82
83    pub fn number(mut self, key: &str, value: i32, compare: Compare) -> Self {
84        self.numbers.push((key.to_string(), value, compare));
85        self
86    }
87
88    pub fn near(mut self, key: &str, value: i32) -> Self {
89        self.near.push((key.to_string(), value));
90        self
91    }
92
93    pub fn open_slots(mut self, slots: u8) -> Self {
94        self.open_slots = Some(slots);
95        self
96    }
97
98    pub fn distance(mut self, distance: Distance) -> Self {
99        self.distance = Some(distance);
100        self
101    }
102
103    pub fn count(mut self, count: u32) -> Self {
104        self.count = Some(count);
105        self
106    }
107
108    pub(crate) fn check(&self) -> Result<(), Error> {
109        for (key, value, _) in &self.strings {
110            lobby_key(key)?;
111            lobby_value(value)?;
112        }
113        for (key, _, _) in &self.numbers {
114            lobby_key(key)?;
115        }
116        for (key, _) in &self.near {
117            lobby_key(key)?;
118        }
119        Ok(())
120    }
121
122    pub(crate) fn matches(&self, data: &BTreeMap<String, String>, open: u32) -> bool {
123        if let Some(slots) = self.open_slots
124            && open < u32::from(slots)
125        {
126            return false;
127        }
128        let strings = self.strings.iter().all(|(key, value, compare)| {
129            data.get(key)
130                .is_some_and(|have| compare.holds(have.as_str().cmp(value.as_str())))
131        });
132        let numbers = self.numbers.iter().all(|(key, value, compare)| {
133            data.get(key)
134                .and_then(|have| have.parse::<i32>().ok())
135                .is_some_and(|have| compare.holds(have.cmp(value)))
136        });
137        strings && numbers
138    }
139
140    pub(crate) fn nearness(&self, data: &BTreeMap<String, String>) -> Vec<u64> {
141        self.near
142            .iter()
143            .map(|(key, value)| {
144                data.get(key)
145                    .and_then(|have| have.parse::<i32>().ok())
146                    .map(|have| i64::from(have).abs_diff(i64::from(*value)))
147                    .unwrap_or(u64::MAX)
148            })
149            .collect()
150    }
151
152    pub(crate) fn limit(&self) -> usize {
153        self.count.unwrap_or(LOBBY_RESULTS) as usize
154    }
155}
156
157pub fn lobby_code(seed: u64) -> String {
158    let mut state = seed;
159    (0..LOBBY_CODE_LEN)
160        .map(|_| {
161            let pick = draw(&mut state) % CODE_LETTERS.len() as u64;
162            CODE_LETTERS[pick as usize] as char
163        })
164        .collect()
165}
166
167pub fn normal_code(code: &str) -> String {
168    code.chars()
169        .filter(|letter| !letter.is_whitespace() && *letter != '-')
170        .map(|letter| letter.to_ascii_uppercase())
171        .collect()
172}
173
174pub fn set_lobby_code(steam: &mut impl SteamApi, lobby: Lobby, code: &str) -> Result<(), Error> {
175    let code = normal_code(code);
176    if code.is_empty() {
177        return Err(Error::Rejected);
178    }
179    steam.set_lobby_data(lobby, LOBBY_CODE_KEY, &code)
180}
181
182pub fn find_lobby_by_code(steam: &mut impl SteamApi, code: &str) -> Result<Call, Error> {
183    let code = normal_code(code);
184    if code.is_empty() {
185        return Err(Error::Rejected);
186    }
187    steam.find_lobbies(
188        &LobbyFilter::default()
189            .string(LOBBY_CODE_KEY, &code, Compare::Equal)
190            .distance(Distance::Worldwide)
191            .count(1),
192    )
193}