1use std::collections::{HashMap, VecDeque};
15
16#[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#[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 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#[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#[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
138pub 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 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#[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 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 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#[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 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#[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 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#[derive(Debug, Clone, Default)]
372pub struct NetInput {
373 pub frame: u64,
374 pub player: u8,
375 pub buttons: u32, pub axes: [i16; 4], 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
413pub struct RollbackState {
415 pub max_rollback: usize,
416 pub current_frame: u64,
417 confirmed: VecDeque<Vec<NetInput>>,
419 predicted: HashMap<(u64, u8), NetInput>,
421 pending: Vec<NetInput>,
423 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 pub fn add_remote_input(&mut self, input: NetInput) {
442 if input.frame < self.current_frame {
444 let rollback_to = input.frame;
445 self.rollback_log.push(rollback_to);
446 }
447 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 pub fn get_input(&self, frame: u64, player: u8) -> NetInput {
461 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 self.predicted.get(&(frame, player))
469 .cloned()
470 .unwrap_or_else(|| NetInput::new(frame, player))
471 }
472
473 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 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#[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"); 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()); 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"))); 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 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}