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}