Skip to main content

steel_core/player/chat/
message_validator.rs

1//! Server-side validation of client message acknowledgements.
2
3use std::collections::VecDeque;
4
5/// Maximum number of tracked messages for acknowledgement validation (Vanilla: 20)
6const MAX_TRACKED_MESSAGES: usize = 20;
7
8/// Entry tracking a sent message signature
9#[derive(Debug, Clone)]
10struct TrackedEntry {
11    signature: Option<Box<[u8]>>,
12    pending: bool, // true if not yet acknowledged by client
13}
14
15impl TrackedEntry {
16    fn acknowledge(self) -> Self {
17        Self {
18            signature: self.signature,
19            pending: false,
20        }
21    }
22}
23
24/// Validates that the client is properly acknowledging messages sent by the server
25#[derive(Debug)]
26pub struct LastSeenMessagesValidator {
27    tracked_messages: VecDeque<Option<TrackedEntry>>,
28    last_pending_signature: Option<Box<[u8]>>,
29}
30
31impl Default for LastSeenMessagesValidator {
32    fn default() -> Self {
33        Self::new()
34    }
35}
36
37impl LastSeenMessagesValidator {
38    /// Creates a new validator
39    #[must_use]
40    pub fn new() -> Self {
41        let mut tracked_messages = VecDeque::with_capacity(MAX_TRACKED_MESSAGES);
42        for _ in 0..MAX_TRACKED_MESSAGES {
43            tracked_messages.push_back(None);
44        }
45        Self {
46            tracked_messages,
47            last_pending_signature: None,
48        }
49    }
50
51    /// Adds a pending message signature that the client should acknowledge
52    /// Matches vanilla's deduplication: only adds if different from last pending message
53    pub fn add_pending(&mut self, signature: Option<Box<[u8]>>) {
54        // Only add if this signature is different from the last one we added
55        // This prevents duplicates when the same message is processed multiple times
56        if signature.as_ref() != self.last_pending_signature.as_ref() {
57            let entry = TrackedEntry {
58                signature: signature.clone(),
59                pending: true,
60            };
61            self.tracked_messages.push_back(Some(entry));
62            self.last_pending_signature = signature;
63        }
64    }
65
66    /// Gets the number of tracked messages
67    #[must_use]
68    pub fn tracked_count(&self) -> usize {
69        self.tracked_messages.len()
70    }
71
72    /// Applies an offset (removes old acknowledged messages from tracking)
73    pub fn apply_offset(&mut self, offset: i32) -> Result<(), String> {
74        let removable = self
75            .tracked_messages
76            .len()
77            .saturating_sub(MAX_TRACKED_MESSAGES);
78        if offset < 0 || offset as usize > removable {
79            return Err(format!(
80                "Advanced last seen window by {offset} messages, but expected at most {removable}"
81            ));
82        }
83        for _ in 0..offset {
84            self.tracked_messages.pop_front();
85        }
86        Ok(())
87    }
88
89    /// Applies an acknowledgement update from the client
90    /// acknowledged: `BitSet` of 20 bits indicating which messages in the window are acknowledged
91    /// offset: How many old messages to remove from the window
92    /// checksum: Optional checksum for validation (0 = skip checksum)
93    pub fn apply_update(
94        &mut self,
95        acknowledged: [u8; 3], // 3 bytes = 24 bits, using 20
96        offset: i32,
97        checksum: u8,
98    ) -> Result<Vec<Box<[u8]>>, String> {
99        log::debug!(
100            "apply_update: offset={}, checksum={}, tracked_messages.len()={}, acknowledged={:?}",
101            offset,
102            checksum,
103            self.tracked_messages.len(),
104            acknowledged
105        );
106
107        // First apply the offset to remove old messages
108        self.apply_offset(offset)?;
109
110        let mut acknowledged_signatures = Vec::new();
111
112        // Process acknowledgements for the tracked window
113        for i in 0..MAX_TRACKED_MESSAGES.min(self.tracked_messages.len()) {
114            let bit_index = i;
115            let byte_index = bit_index / 8;
116            let bit_offset = bit_index % 8;
117            let is_acknowledged = (acknowledged[byte_index] & (1 << bit_offset)) != 0;
118
119            if let Some(entry_opt) = self.tracked_messages.get_mut(i) {
120                if is_acknowledged {
121                    // Client acknowledged this message
122                    if let Some(entry) = entry_opt {
123                        log::debug!(
124                            "Index {}: Client acknowledged message (pending={})",
125                            i,
126                            entry.pending
127                        );
128                        let acknowledged_entry = entry.clone().acknowledge();
129                        if let Some(sig) = &entry.signature {
130                            acknowledged_signatures.push(sig.clone());
131                        }
132                        *entry_opt = Some(acknowledged_entry);
133                    } else {
134                        log::error!(
135                            "Index {i}: Client acknowledged unknown/ignored message! tracked_messages[{i}] = None"
136                        );
137                        return Err(format!(
138                            "Last seen update acknowledged unknown or previously ignored message at index {i}"
139                        ));
140                    }
141                } else {
142                    // Client did not acknowledge this message
143                    if let Some(entry) = entry_opt
144                        && !entry.pending
145                    {
146                        log::error!("Index {i}: Client ignored previously acknowledged message!");
147                        return Err(format!(
148                            "Last seen update ignored previously acknowledged message at index {i}"
149                        ));
150                    }
151                    log::debug!("Index {i}: Client did not acknowledge (setting to None)");
152                    // Set to None if not acknowledged
153                    *entry_opt = None;
154                }
155            }
156        }
157
158        log::debug!(
159            "apply_update: Successfully acknowledged {} signatures",
160            acknowledged_signatures.len()
161        );
162
163        // TODO: Verify checksum if needed (checksum == 0 means skip validation)
164        if checksum != 0 {
165            // For now, we skip checksum validation
166            // In a full implementation, compute checksum of acknowledged_signatures and compare
167        }
168
169        Ok(acknowledged_signatures)
170    }
171}