1use crate::{Change, Delivery, Error, Event, Lobby, SteamApi, User};
2
3pub const RECONNECT_MS: u64 = 120_000;
4const BEAT: u8 = 1;
5const BYE: u8 = 2;
6const BATCH: usize = 64;
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub enum Lost {
10 Silent,
11 SessionFailed(Option<i32>),
12 Disconnected,
13}
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub enum Quit {
17 Goodbye,
18 Left,
19 Kicked,
20 Banned,
21}
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub enum Ending {
25 Quit(Quit),
26 Forfeit(Lost),
27}
28
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30enum Link {
31 Up,
32 Lost { since: u64, lost: Lost },
33 Ended(Ending),
34}
35
36#[derive(Clone, Debug)]
37pub struct Health {
38 peer: User,
39 lobby: Option<Lobby>,
40 channel: u32,
41 every_ms: u64,
42 timeout_ms: u64,
43 window_ms: u64,
44 sent: Option<u64>,
45 heard: u64,
46 link: Link,
47}
48
49impl Health {
50 pub fn new(
51 peer: User,
52 channel: u32,
53 every_ms: u64,
54 timeout_ms: u64,
55 now_ms: u64,
56 ) -> Result<Self, Error> {
57 if every_ms == 0 || timeout_ms <= every_ms {
58 return Err(Error::Rejected);
59 }
60 Ok(Self {
61 peer,
62 lobby: None,
63 channel,
64 every_ms,
65 timeout_ms,
66 window_ms: RECONNECT_MS,
67 sent: None,
68 heard: now_ms,
69 link: Link::Up,
70 })
71 }
72
73 pub fn in_lobby(mut self, lobby: Lobby) -> Self {
74 self.lobby = Some(lobby);
75 self
76 }
77
78 pub fn reconnect_window(mut self, window_ms: u64) -> Self {
79 self.window_ms = window_ms;
80 self
81 }
82
83 pub fn peer(&self) -> User {
84 self.peer
85 }
86
87 pub fn lost(&self) -> Option<Lost> {
88 match self.link {
89 Link::Lost { lost, .. } => Some(lost),
90 _ => None,
91 }
92 }
93
94 pub fn ended(&self) -> Option<Ending> {
95 match self.link {
96 Link::Ended(ending) => Some(ending),
97 _ => None,
98 }
99 }
100
101 pub fn heard(&mut self, now_ms: u64) -> Option<Event> {
102 self.heard = self.heard.max(now_ms);
103 match self.link {
104 Link::Lost { .. } => {
105 self.link = Link::Up;
106 Some(Event::Reconnected { peer: self.peer })
107 }
108 _ => None,
109 }
110 }
111
112 pub fn tick(&mut self, steam: &mut impl SteamApi, now_ms: u64) -> Result<Vec<Event>, Error> {
113 let mut events = Vec::new();
114 if self.ended().is_some() {
115 return Ok(events);
116 }
117 let peer = self.peer;
118 loop {
119 let batch = steam.receive_messages(self.channel, BATCH)?;
120 if batch.is_empty() {
121 break;
122 }
123 for message in batch.into_iter().filter(|message| message.peer == peer) {
124 if message.bytes == [BYE] {
125 events.push(self.quit(Quit::Goodbye));
126 return Ok(events);
127 }
128 events.extend(self.heard(now_ms));
129 }
130 }
131 if self
132 .sent
133 .is_none_or(|sent| now_ms.saturating_sub(sent) >= self.every_ms)
134 {
135 steam.send_message(self.peer, self.channel, &[BEAT], Delivery::Reliable)?;
136 self.sent = Some(now_ms);
137 }
138 match self.link {
139 Link::Up if now_ms.saturating_sub(self.heard) >= self.timeout_ms => {
140 events.extend(self.lose(Lost::Silent, now_ms));
141 }
142 Link::Lost { since, lost } if now_ms.saturating_sub(since) >= self.window_ms => {
143 self.link = Link::Ended(Ending::Forfeit(lost));
144 events.push(Event::Forfeit { peer, lost });
145 }
146 _ => {}
147 }
148 Ok(events)
149 }
150
151 pub fn observe(&mut self, event: &Event, now_ms: u64) -> Option<Event> {
152 if self.ended().is_some() {
153 return None;
154 }
155 match event {
156 Event::SessionFailed { peer, reason } if *peer == self.peer => {
157 self.lose(Lost::SessionFailed(*reason), now_ms)
158 }
159 Event::Member {
160 lobby,
161 user,
162 change,
163 } if *user == self.peer && Some(*lobby) == self.lobby => match change {
164 Change::Entered => self.heard(now_ms),
165 Change::Disconnected => self.lose(Lost::Disconnected, now_ms),
166 Change::Left => Some(self.quit(Quit::Left)),
167 Change::Kicked => Some(self.quit(Quit::Kicked)),
168 Change::Banned => Some(self.quit(Quit::Banned)),
169 },
170 _ => None,
171 }
172 }
173
174 pub fn leave(&mut self, steam: &mut impl SteamApi) -> Result<(), Error> {
175 if self.ended().is_some() {
176 return Ok(());
177 }
178 self.link = Link::Ended(Ending::Quit(Quit::Goodbye));
179 steam.send_message(self.peer, self.channel, &[BYE], Delivery::Reliable)
180 }
181
182 fn lose(&mut self, lost: Lost, now_ms: u64) -> Option<Event> {
183 if self.link != Link::Up {
184 return None;
185 }
186 self.link = Link::Lost {
187 since: now_ms,
188 lost,
189 };
190 Some(Event::ConnectionLost {
191 peer: self.peer,
192 lost,
193 })
194 }
195
196 fn quit(&mut self, quit: Quit) -> Event {
197 self.link = Link::Ended(Ending::Quit(quit));
198 Event::PeerQuit {
199 peer: self.peer,
200 quit,
201 }
202 }
203}
204
205pub fn connect_lobby(args: &[String]) -> Option<Lobby> {
206 args.windows(2)
207 .find(|pair| pair[0] == "+connect_lobby")
208 .and_then(|pair| pair[1].parse::<u64>().ok())
209 .filter(|id| *id != 0)
210 .map(Lobby)
211}