Skip to main content

pfx_steam/
health.rs

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}