Skip to main content

proof_engine/network/
mod.rs

1//! Network message types for Proof Engine, with in-memory stand-ins.
2//!
3//! Nothing in this module opens a socket: the "clients" here queue and
4//! inspect messages in memory, which is useful for tests and replays. For
5//! real HTTP and WebSocket I/O use [`crate::networking`] with the `http` /
6//! `websocket` features. Contents:
7//! - HTTP request builder (GET/POST/PUT/DELETE)
8//! - WebSocket message protocol
9//! - Leaderboard submission and retrieval
10//! - Cloud save serialization
11//! - Lobby system messages
12//! - Rollback netcode data structures
13
14use std::collections::{HashMap, VecDeque};
15
16// ── HttpMethod ────────────────────────────────────────────────────────────────
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum HttpMethod { Get, Post, Put, Delete, Patch, Head }
20
21impl HttpMethod {
22    pub fn as_str(self) -> &'static str {
23        match self {
24            HttpMethod::Get    => "GET",
25            HttpMethod::Post   => "POST",
26            HttpMethod::Put    => "PUT",
27            HttpMethod::Delete => "DELETE",
28            HttpMethod::Patch  => "PATCH",
29            HttpMethod::Head   => "HEAD",
30        }
31    }
32}
33
34// ── HttpRequest ───────────────────────────────────────────────────────────────
35
36#[derive(Debug, Clone)]
37pub struct HttpRequest {
38    pub method:  HttpMethod,
39    pub url:     String,
40    pub headers: HashMap<String, String>,
41    pub body:    Option<Vec<u8>>,
42    pub timeout_ms: u32,
43}
44
45impl HttpRequest {
46    pub fn get(url: &str) -> Self {
47        Self { method: HttpMethod::Get, url: url.to_string(), headers: HashMap::new(), body: None, timeout_ms: 5000 }
48    }
49
50    pub fn post(url: &str, body: Vec<u8>) -> Self {
51        let mut req = Self::get(url);
52        req.method = HttpMethod::Post;
53        req.body = Some(body);
54        req
55    }
56
57    pub fn post_json(url: &str, json: &str) -> Self {
58        let mut req = Self::post(url, json.as_bytes().to_vec());
59        req.headers.insert("Content-Type".to_string(), "application/json".to_string());
60        req
61    }
62
63    pub fn with_header(mut self, key: &str, value: &str) -> Self {
64        self.headers.insert(key.to_string(), value.to_string());
65        self
66    }
67
68    pub fn with_bearer(mut self, token: &str) -> Self {
69        self.headers.insert("Authorization".to_string(), format!("Bearer {}", token));
70        self
71    }
72
73    pub fn with_timeout(mut self, ms: u32) -> Self { self.timeout_ms = ms; self }
74
75    /// Encode as a simple textual representation (for debugging/logging).
76    pub fn to_string(&self) -> String {
77        format!("{} {} (body: {} bytes)",
78            self.method.as_str(), self.url,
79            self.body.as_ref().map(|b| b.len()).unwrap_or(0))
80    }
81}
82
83// ── HttpResponse ──────────────────────────────────────────────────────────────
84
85#[derive(Debug, Clone)]
86pub struct HttpResponse {
87    pub status:  u16,
88    pub headers: HashMap<String, String>,
89    pub body:    Vec<u8>,
90    pub latency_ms: u32,
91}
92
93impl HttpResponse {
94    pub fn ok(body: Vec<u8>) -> Self {
95        Self { status: 200, headers: HashMap::new(), body, latency_ms: 0 }
96    }
97
98    pub fn error(status: u16, message: &str) -> Self {
99        Self { status, headers: HashMap::new(), body: message.as_bytes().to_vec(), latency_ms: 0 }
100    }
101
102    pub fn body_str(&self) -> &str {
103        std::str::from_utf8(&self.body).unwrap_or("")
104    }
105
106    pub fn is_success(&self) -> bool { self.status >= 200 && self.status < 300 }
107    pub fn is_client_error(&self) -> bool { self.status >= 400 && self.status < 500 }
108    pub fn is_server_error(&self) -> bool { self.status >= 500 }
109}
110
111// ── WebSocket ─────────────────────────────────────────────────────────────────
112
113#[derive(Debug, Clone, PartialEq)]
114pub enum WsMessage {
115    Text(String),
116    Binary(Vec<u8>),
117    Ping(Vec<u8>),
118    Pong(Vec<u8>),
119    Close(Option<(u16, String)>),
120}
121
122impl WsMessage {
123    pub fn text(s: &str) -> Self { Self::Text(s.to_string()) }
124    pub fn json(s: &str) -> Self { Self::Text(s.to_string()) }
125    pub fn binary(data: Vec<u8>) -> Self { Self::Binary(data) }
126
127    pub fn is_text(&self) -> bool { matches!(self, WsMessage::Text(_)) }
128    pub fn is_binary(&self) -> bool { matches!(self, WsMessage::Binary(_)) }
129
130    pub fn as_text(&self) -> Option<&str> {
131        if let WsMessage::Text(s) = self { Some(s.as_str()) } else { None }
132    }
133}
134
135#[derive(Debug, Clone, Copy, PartialEq)]
136pub enum WsState { Connecting, Open, Closing, Closed }
137
138/// Mock WebSocket client with a pending message queue.
139pub struct WebSocket {
140    pub url:     String,
141    pub state:   WsState,
142    outgoing:    VecDeque<WsMessage>,
143    incoming:    VecDeque<WsMessage>,
144    on_error:    Option<String>,
145    pub protocol: String,
146}
147
148impl WebSocket {
149    pub fn new(url: &str) -> Self {
150        Self { url: url.to_string(), state: WsState::Connecting,
151               outgoing: VecDeque::new(), incoming: VecDeque::new(),
152               on_error: None, protocol: String::new() }
153    }
154
155    pub fn open(&mut self) { self.state = WsState::Open; }
156    pub fn close(&mut self) { self.state = WsState::Closing; }
157
158    pub fn send(&mut self, msg: WsMessage) -> bool {
159        if self.state != WsState::Open { return false; }
160        self.outgoing.push_back(msg);
161        true
162    }
163
164    pub fn send_text(&mut self, text: &str) -> bool { self.send(WsMessage::text(text)) }
165    pub fn send_json(&mut self, json: &str) -> bool { self.send(WsMessage::json(json)) }
166
167    pub fn recv(&mut self) -> Option<WsMessage> { self.incoming.pop_front() }
168
169    /// Inject a received message (for testing / server push).
170    pub fn inject_message(&mut self, msg: WsMessage) { self.incoming.push_back(msg); }
171
172    pub fn has_pending_send(&self) -> bool { !self.outgoing.is_empty() }
173    pub fn drain_outgoing(&mut self) -> Vec<WsMessage> { self.outgoing.drain(..).collect() }
174    pub fn is_open(&self) -> bool { self.state == WsState::Open }
175}
176
177// ── Leaderboard ───────────────────────────────────────────────────────────────
178
179#[derive(Debug, Clone)]
180pub struct LeaderboardEntry {
181    pub rank:       u32,
182    pub player_id:  String,
183    pub display_name: String,
184    pub score:      i64,
185    pub metadata:   HashMap<String, String>,
186    pub timestamp:  u64,
187}
188
189#[derive(Debug, Clone)]
190pub struct Leaderboard {
191    pub id:      String,
192    pub name:    String,
193    pub entries: Vec<LeaderboardEntry>,
194    pub page:    u32,
195    pub total:   u32,
196}
197
198impl Leaderboard {
199    pub fn new(id: &str, name: &str) -> Self {
200        Self { id: id.to_string(), name: name.to_string(), entries: Vec::new(), page: 0, total: 0 }
201    }
202
203    pub fn add_entry(&mut self, entry: LeaderboardEntry) {
204        self.entries.push(entry);
205        self.entries.sort_by(|a, b| b.score.cmp(&a.score));
206        // Re-rank
207        for (i, e) in self.entries.iter_mut().enumerate() { e.rank = (i + 1) as u32; }
208    }
209
210    pub fn top_n(&self, n: usize) -> &[LeaderboardEntry] {
211        &self.entries[..n.min(self.entries.len())]
212    }
213
214    pub fn rank_of(&self, player_id: &str) -> Option<u32> {
215        self.entries.iter().find(|e| e.player_id == player_id).map(|e| e.rank)
216    }
217
218    /// Serialize to JSON-like string for API submission.
219    pub fn submission_json(&self, player_id: &str, score: i64, metadata: &HashMap<String, String>) -> String {
220        let meta_str: Vec<String> = metadata.iter().map(|(k, v)| format!("\"{}\":\"{}\"", k, v)).collect();
221        format!(
222            "{{\"leaderboard\":\"{}\",\"player_id\":\"{}\",\"score\":{},\"metadata\":{{{}}}}}",
223            self.id, player_id, score, meta_str.join(",")
224        )
225    }
226}
227
228// ── CloudSave ─────────────────────────────────────────────────────────────────
229
230#[derive(Debug, Clone)]
231pub struct CloudSave {
232    pub player_id:   String,
233    pub slot:        u8,
234    pub version:     u32,
235    pub created_at:  u64,
236    pub updated_at:  u64,
237    pub data:        Vec<u8>,
238    pub checksum:    u32,
239    pub tags:        Vec<String>,
240    pub metadata:    HashMap<String, String>,
241}
242
243impl CloudSave {
244    pub fn new(player_id: &str, slot: u8, data: Vec<u8>) -> Self {
245        let checksum = simple_checksum(&data);
246        Self {
247            player_id: player_id.to_string(), slot, version: 1,
248            created_at: 0, updated_at: 0,
249            data, checksum, tags: Vec::new(), metadata: HashMap::new(),
250        }
251    }
252
253    pub fn validate(&self) -> bool {
254        simple_checksum(&self.data) == self.checksum
255    }
256
257    pub fn size_bytes(&self) -> usize { self.data.len() }
258
259    /// Encode to bytes for upload.
260    pub fn encode(&self) -> Vec<u8> {
261        let mut out = Vec::new();
262        out.extend_from_slice(b"CSAVE1\x00\x00");
263        out.push(self.slot);
264        out.extend_from_slice(&self.version.to_le_bytes());
265        out.extend_from_slice(&self.checksum.to_le_bytes());
266        out.extend_from_slice(&(self.data.len() as u32).to_le_bytes());
267        out.extend_from_slice(&self.data);
268        out
269    }
270
271    pub fn decode(bytes: &[u8]) -> Option<Self> {
272        if bytes.len() < 20 || &bytes[0..6] != b"CSAVE1" { return None; }
273        let slot = bytes[8];
274        let version = u32::from_le_bytes(bytes[9..13].try_into().ok()?);
275        let checksum = u32::from_le_bytes(bytes[13..17].try_into().ok()?);
276        let data_len = u32::from_le_bytes(bytes[17..21].try_into().ok()?) as usize;
277        if bytes.len() < 21 + data_len { return None; }
278        let data = bytes[21..21 + data_len].to_vec();
279        Some(Self { player_id: String::new(), slot, version, created_at: 0, updated_at: 0,
280                    data, checksum, tags: Vec::new(), metadata: HashMap::new() })
281    }
282}
283
284fn simple_checksum(data: &[u8]) -> u32 {
285    data.iter().enumerate().fold(0u32, |acc, (i, &b)| {
286        acc.wrapping_add((b as u32).wrapping_mul((i as u32).wrapping_add(1)))
287    })
288}
289
290// ── Lobby ─────────────────────────────────────────────────────────────────────
291
292#[derive(Debug, Clone, Copy, PartialEq)]
293pub enum LobbyState { Open, InProgress, Closed }
294
295#[derive(Debug, Clone)]
296pub struct LobbyPlayer {
297    pub player_id:    String,
298    pub display_name: String,
299    pub ready:        bool,
300    pub latency_ms:   u32,
301    pub team:         u8,
302    pub metadata:     HashMap<String, String>,
303}
304
305impl LobbyPlayer {
306    pub fn new(id: &str, name: &str) -> Self {
307        Self { player_id: id.to_string(), display_name: name.to_string(),
308               ready: false, latency_ms: 0, team: 0, metadata: HashMap::new() }
309    }
310}
311
312#[derive(Debug, Clone)]
313pub struct Lobby {
314    pub id:      String,
315    pub name:    String,
316    pub state:   LobbyState,
317    pub players: Vec<LobbyPlayer>,
318    pub max_players: u8,
319    pub host_id: String,
320    pub settings: HashMap<String, String>,
321}
322
323impl Lobby {
324    pub fn new(id: &str, name: &str, max_players: u8) -> Self {
325        Self { id: id.to_string(), name: name.to_string(), state: LobbyState::Open,
326               players: Vec::new(), max_players, host_id: String::new(), settings: HashMap::new() }
327    }
328
329    pub fn join(&mut self, player: LobbyPlayer) -> bool {
330        if self.players.len() >= self.max_players as usize { return false; }
331        if self.state != LobbyState::Open { return false; }
332        if self.players.iter().any(|p| p.player_id == player.player_id) { return false; }
333        if self.players.is_empty() {
334            self.host_id = player.player_id.clone();
335        }
336        self.players.push(player);
337        true
338    }
339
340    pub fn leave(&mut self, player_id: &str) {
341        self.players.retain(|p| p.player_id != player_id);
342        // Transfer host if needed
343        if self.host_id == player_id && !self.players.is_empty() {
344            self.host_id = self.players[0].player_id.clone();
345        }
346    }
347
348    pub fn set_ready(&mut self, player_id: &str, ready: bool) {
349        if let Some(p) = self.players.iter_mut().find(|p| p.player_id == player_id) {
350            p.ready = ready;
351        }
352    }
353
354    pub fn all_ready(&self) -> bool {
355        !self.players.is_empty() && self.players.iter().all(|p| p.ready)
356    }
357
358    pub fn start(&mut self) -> bool {
359        if !self.all_ready() { return false; }
360        self.state = LobbyState::InProgress;
361        true
362    }
363
364    pub fn player_count(&self) -> usize { self.players.len() }
365    pub fn is_full(&self) -> bool { self.players.len() >= self.max_players as usize }
366}
367
368// ── Rollback Netcode ──────────────────────────────────────────────────────────
369
370/// Input for one player at a given frame.
371#[derive(Debug, Clone, Default)]
372pub struct NetInput {
373    pub frame:   u64,
374    pub player:  u8,
375    pub buttons: u32, // bitfield
376    pub axes:    [i16; 4], // fixed-point axes
377    pub checksum: u16,
378}
379
380impl NetInput {
381    pub fn new(frame: u64, player: u8) -> Self {
382        Self { frame, player, ..Default::default() }
383    }
384
385    pub fn press_button(&mut self, btn: u8) { self.buttons |= 1 << btn; }
386    pub fn release_button(&mut self, btn: u8) { self.buttons &= !(1 << btn); }
387    pub fn is_pressed(&self, btn: u8) -> bool { (self.buttons >> btn) & 1 != 0 }
388    pub fn set_axis(&mut self, idx: usize, value: f32) {
389        if idx < 4 { self.axes[idx] = (value.clamp(-1.0, 1.0) * 32767.0) as i16; }
390    }
391    pub fn get_axis(&self, idx: usize) -> f32 {
392        if idx < 4 { self.axes[idx] as f32 / 32767.0 } else { 0.0 }
393    }
394
395    pub fn encode(&self) -> [u8; 16] {
396        let mut buf = [0u8; 16];
397        buf[0..8].copy_from_slice(&self.frame.to_le_bytes());
398        buf[8]  = self.player;
399        buf[9..13].copy_from_slice(&self.buttons.to_le_bytes());
400        buf[13..15].copy_from_slice(&self.axes[0].to_le_bytes());
401        buf[15] = (self.checksum & 0xFF) as u8;
402        buf
403    }
404
405    pub fn decode(buf: &[u8; 16]) -> Self {
406        let frame   = u64::from_le_bytes(buf[0..8].try_into().unwrap());
407        let player  = buf[8];
408        let buttons = u32::from_le_bytes(buf[9..13].try_into().unwrap());
409        Self { frame, player, buttons, axes: [0; 4], checksum: buf[15] as u16 }
410    }
411}
412
413/// Rollback netcode state: stores a history of inputs for re-simulation.
414pub struct RollbackState {
415    pub max_rollback:   usize,
416    pub current_frame:  u64,
417    /// Confirmed inputs per frame per player.
418    confirmed:          VecDeque<Vec<NetInput>>,
419    /// Predicted inputs (used when real inputs not yet received).
420    predicted:          HashMap<(u64, u8), NetInput>,
421    /// Pending remote inputs waiting for confirmation.
422    pending:            Vec<NetInput>,
423    /// Frame numbers where rollback was needed.
424    pub rollback_log:   Vec<u64>,
425    pub player_count:   usize,
426}
427
428impl RollbackState {
429    pub fn new(player_count: usize, max_rollback: usize) -> Self {
430        Self {
431            max_rollback, current_frame: 0,
432            confirmed: VecDeque::new(),
433            predicted: HashMap::new(),
434            pending: Vec::new(),
435            rollback_log: Vec::new(),
436            player_count,
437        }
438    }
439
440    /// Add a confirmed input from a remote player.
441    pub fn add_remote_input(&mut self, input: NetInput) {
442        // If this input's frame is in the past, we need to rollback
443        if input.frame < self.current_frame {
444            let rollback_to = input.frame;
445            self.rollback_log.push(rollback_to);
446        }
447        // Store confirmed input
448        let frame = input.frame;
449        while self.confirmed.len() <= frame as usize {
450            self.confirmed.push_back(Vec::new());
451        }
452        if (frame as usize) < self.confirmed.len() {
453            let player = input.player;
454            self.confirmed[frame as usize].push(input);
455            self.predicted.remove(&(frame, player));
456        }
457    }
458
459    /// Get input for (frame, player): confirmed if available, else predicted.
460    pub fn get_input(&self, frame: u64, player: u8) -> NetInput {
461        // Check confirmed
462        if let Some(inputs) = self.confirmed.get(frame as usize) {
463            if let Some(inp) = inputs.iter().find(|i| i.player == player) {
464                return inp.clone();
465            }
466        }
467        // Return prediction
468        self.predicted.get(&(frame, player))
469            .cloned()
470            .unwrap_or_else(|| NetInput::new(frame, player))
471    }
472
473    /// Predict next frame's input by repeating current frame's input.
474    pub fn predict_input(&mut self, frame: u64, player: u8) {
475        let prev = self.get_input(frame.saturating_sub(1), player);
476        let mut pred = prev;
477        pred.frame = frame;
478        self.predicted.insert((frame, player), pred);
479    }
480
481    pub fn advance_frame(&mut self) {
482        for p in 0..self.player_count as u8 {
483            self.predict_input(self.current_frame + 1, p);
484        }
485        self.current_frame += 1;
486        // Trim old confirmed data
487        while self.confirmed.len() > self.max_rollback {
488            self.confirmed.pop_front();
489        }
490    }
491
492    pub fn needs_rollback(&self) -> bool { !self.rollback_log.is_empty() }
493
494    pub fn consume_rollback(&mut self) -> Option<u64> { self.rollback_log.pop() }
495}
496
497// ── Tests ─────────────────────────────────────────────────────────────────────
498
499#[cfg(test)]
500mod tests {
501    use super::*;
502
503    #[test]
504    fn test_http_request_builder() {
505        let req = HttpRequest::get("https://api.example.com/scores")
506            .with_header("Accept", "application/json")
507            .with_bearer("token123")
508            .with_timeout(3000);
509        assert_eq!(req.method, HttpMethod::Get);
510        assert_eq!(req.timeout_ms, 3000);
511        assert!(req.headers.contains_key("Authorization"));
512    }
513
514    #[test]
515    fn test_http_response_status() {
516        let ok = HttpResponse::ok(b"{}".to_vec());
517        assert!(ok.is_success());
518        let err = HttpResponse::error(404, "Not Found");
519        assert!(err.is_client_error());
520        let srv = HttpResponse::error(500, "Internal Server Error");
521        assert!(srv.is_server_error());
522    }
523
524    #[test]
525    fn test_websocket_send_recv() {
526        let mut ws = WebSocket::new("wss://echo.example.com");
527        ws.open();
528        assert!(ws.is_open());
529
530        ws.send_text("hello");
531        assert!(ws.has_pending_send());
532        let out = ws.drain_outgoing();
533        assert_eq!(out.len(), 1);
534
535        ws.inject_message(WsMessage::text("world"));
536        let msg = ws.recv().unwrap();
537        assert_eq!(msg.as_text(), Some("world"));
538    }
539
540    #[test]
541    fn test_leaderboard_ranking() {
542        let mut lb = Leaderboard::new("speed_run", "Speed Run");
543        lb.add_entry(LeaderboardEntry { rank: 0, player_id: "a".to_string(), display_name: "Alice".to_string(), score: 500, metadata: HashMap::new(), timestamp: 0 });
544        lb.add_entry(LeaderboardEntry { rank: 0, player_id: "b".to_string(), display_name: "Bob".to_string(), score: 800, metadata: HashMap::new(), timestamp: 0 });
545        lb.add_entry(LeaderboardEntry { rank: 0, player_id: "c".to_string(), display_name: "Carol".to_string(), score: 650, metadata: HashMap::new(), timestamp: 0 });
546
547        assert_eq!(lb.entries[0].display_name, "Bob");
548        assert_eq!(lb.rank_of("a"), Some(3));
549        assert_eq!(lb.rank_of("b"), Some(1));
550    }
551
552    #[test]
553    fn test_cloud_save_encode_decode() {
554        let save = CloudSave::new("player1", 0, vec![1, 2, 3, 4, 5]);
555        assert!(save.validate());
556        let encoded = save.encode();
557        let decoded = CloudSave::decode(&encoded).unwrap();
558        assert_eq!(decoded.data, vec![1, 2, 3, 4, 5]);
559        assert!(decoded.validate());
560    }
561
562    #[test]
563    fn test_lobby_join_leave() {
564        let mut lobby = Lobby::new("room1", "Test Room", 4);
565        assert!(lobby.join(LobbyPlayer::new("p1", "Alice")));
566        assert!(lobby.join(LobbyPlayer::new("p2", "Bob")));
567        assert_eq!(lobby.player_count(), 2);
568        assert_eq!(lobby.host_id, "p1");
569
570        lobby.leave("p1");
571        assert_eq!(lobby.host_id, "p2"); // host transferred
572        assert_eq!(lobby.player_count(), 1);
573    }
574
575    #[test]
576    fn test_lobby_ready_and_start() {
577        let mut lobby = Lobby::new("r2", "Room", 2);
578        lobby.join(LobbyPlayer::new("p1", "A"));
579        lobby.join(LobbyPlayer::new("p2", "B"));
580        assert!(!lobby.start()); // not all ready
581        lobby.set_ready("p1", true);
582        lobby.set_ready("p2", true);
583        assert!(lobby.start());
584        assert_eq!(lobby.state, LobbyState::InProgress);
585    }
586
587    #[test]
588    fn test_lobby_max_players() {
589        let mut lobby = Lobby::new("r3", "Room", 2);
590        assert!(lobby.join(LobbyPlayer::new("p1", "A")));
591        assert!(lobby.join(LobbyPlayer::new("p2", "B")));
592        assert!(!lobby.join(LobbyPlayer::new("p3", "C"))); // full
593        assert!(lobby.is_full());
594    }
595
596    #[test]
597    fn test_net_input_encode_decode() {
598        let mut input = NetInput::new(42, 1);
599        input.press_button(3);
600        input.set_axis(0, 0.75);
601        let encoded = input.encode();
602        let decoded = NetInput::decode(&encoded);
603        assert_eq!(decoded.frame, 42);
604        assert_eq!(decoded.player, 1);
605        assert!(decoded.is_pressed(3));
606    }
607
608    #[test]
609    fn test_rollback_state_predict() {
610        let mut rb = RollbackState::new(2, 8);
611        rb.advance_frame();
612        let inp = rb.get_input(1, 0);
613        assert_eq!(inp.frame, 1);
614    }
615
616    #[test]
617    fn test_rollback_detects_mismatch() {
618        let mut rb = RollbackState::new(2, 8);
619        rb.advance_frame();
620        rb.advance_frame();
621        // Remote player sends input for frame 1 (in the past)
622        let remote = NetInput::new(1, 1);
623        rb.add_remote_input(remote);
624        assert!(rb.needs_rollback());
625        let frame = rb.consume_rollback();
626        assert_eq!(frame, Some(1));
627    }
628}