1use std::collections::{HashMap, VecDeque};
12
13#[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#[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 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#[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#[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
135pub 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 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#[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 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 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#[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 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#[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 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#[derive(Debug, Clone, Default)]
369pub struct NetInput {
370 pub frame: u64,
371 pub player: u8,
372 pub buttons: u32, pub axes: [i16; 4], 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
410pub struct RollbackState {
412 pub max_rollback: usize,
413 pub current_frame: u64,
414 confirmed: VecDeque<Vec<NetInput>>,
416 predicted: HashMap<(u64, u8), NetInput>,
418 pending: Vec<NetInput>,
420 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 pub fn add_remote_input(&mut self, input: NetInput) {
439 if input.frame < self.current_frame {
441 let rollback_to = input.frame;
442 self.rollback_log.push(rollback_to);
443 }
444 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 pub fn get_input(&self, frame: u64, player: u8) -> NetInput {
458 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 self.predicted.get(&(frame, player))
466 .cloned()
467 .unwrap_or_else(|| NetInput::new(frame, player))
468 }
469
470 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 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#[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"); 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()); 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"))); 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 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}