Skip to main content

proof_engine/network/
mod.rs

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