1use std::collections::{HashMap, VecDeque};
8
9#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum CloudProvider {
16 None,
17 SteamCloud,
18 EpicOnline,
19 CustomServer(String),
20}
21
22impl CloudProvider {
23 pub fn is_enabled(&self) -> bool {
24 !matches!(self, CloudProvider::None)
25 }
26
27 pub fn display_name(&self) -> String {
28 match self {
29 CloudProvider::None => "None".into(),
30 CloudProvider::SteamCloud => "Steam Cloud".into(),
31 CloudProvider::EpicOnline => "Epic Online".into(),
32 CloudProvider::CustomServer(url) => format!("Custom ({})", url),
33 }
34 }
35}
36
37#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct CloudSaveEntry {
44 pub slot_id: u32,
45 pub user_id: String,
46 pub save_data: Vec<u8>,
47 pub timestamp: u64,
48 pub checksum: u32,
49 pub size_bytes: usize,
50 pub conflict_resolved: bool,
51}
52
53impl CloudSaveEntry {
54 pub fn new(slot_id: u32, user_id: impl Into<String>, save_data: Vec<u8>, timestamp: u64) -> Self {
55 let checksum = simple_checksum(&save_data);
56 let size_bytes = save_data.len();
57 Self { slot_id, user_id: user_id.into(), save_data, timestamp, checksum, size_bytes, conflict_resolved: false }
58 }
59
60 pub fn verify_checksum(&self) -> bool {
61 simple_checksum(&self.save_data) == self.checksum
62 }
63}
64
65fn simple_checksum(data: &[u8]) -> u32 {
66 let mut crc = 0u32;
67 for &b in data {
68 crc = crc.wrapping_add(b as u32).rotate_left(3);
69 }
70 crc
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum ConflictResolution {
80 LocalWins,
81 RemoteWins,
82 MergeByTimestamp,
83 MergeByVersion,
84 UserChoice,
85}
86
87#[derive(Debug, Clone)]
93pub enum CloudSyncState {
94 Idle,
95 Uploading(f32),
96 Downloading(f32),
97 Conflict { local: CloudSaveEntry, remote: CloudSaveEntry },
98 SyncComplete,
99 Error(String),
100}
101
102#[derive(Debug, Clone, Default)]
108pub struct CloudMetadata {
109 pub last_sync_time: u64,
110 pub total_slots: u32,
111 pub used_bytes: u64,
112 pub quota_bytes: u64,
113}
114
115impl CloudMetadata {
116 pub fn usage_fraction(&self) -> f32 {
117 if self.quota_bytes == 0 { return 0.0; }
118 (self.used_bytes as f32) / (self.quota_bytes as f32)
119 }
120
121 pub fn has_space(&self, needed: u64) -> bool {
122 self.used_bytes + needed <= self.quota_bytes
123 }
124}
125
126#[derive(Debug, Clone, Default)]
128pub struct CloudSaveStats {
129 pub uploads: u64,
130 pub downloads: u64,
131 pub conflicts_resolved: u64,
132 pub bytes_transferred: u64,
133}
134
135impl CloudSaveStats {
136 pub fn record_upload(&mut self, bytes: usize) {
137 self.uploads += 1;
138 self.bytes_transferred += bytes as u64;
139 }
140 pub fn record_download(&mut self, bytes: usize) {
141 self.downloads += 1;
142 self.bytes_transferred += bytes as u64;
143 }
144 pub fn record_conflict_resolved(&mut self) {
145 self.conflicts_resolved += 1;
146 }
147}
148
149#[derive(Debug, Clone)]
155pub enum PendingOp {
156 Upload { slot_id: u32, data: Vec<u8>, timestamp: u64 },
157 Download { slot_id: u32 },
158}
159
160#[derive(Debug, Default)]
163pub struct OfflineQueue {
164 ops: VecDeque<PendingOp>,
165}
166
167impl OfflineQueue {
168 const MAX_OPS: usize = 100;
169
170 pub fn new() -> Self {
171 Self { ops: VecDeque::new() }
172 }
173
174 pub fn push(&mut self, op: PendingOp) {
175 if self.ops.len() >= Self::MAX_OPS {
176 self.ops.pop_front(); }
178 self.ops.push_back(op);
179 }
180
181 pub fn drain(&mut self) -> Vec<PendingOp> {
182 self.ops.drain(..).collect()
183 }
184
185 pub fn len(&self) -> usize {
186 self.ops.len()
187 }
188
189 pub fn is_empty(&self) -> bool {
190 self.ops.is_empty()
191 }
192
193 pub fn is_full(&self) -> bool {
194 self.ops.len() >= Self::MAX_OPS
195 }
196}
197
198pub struct SaveEncryption;
206
207impl SaveEncryption {
208 fn derive_key(user_id: &str) -> Vec<u8> {
210 let base = user_id.as_bytes();
211 if base.is_empty() {
212 return vec![0xAB];
213 }
214 let mut key = base.to_vec();
216 for i in 1..32 {
217 let prev = key[i - 1];
218 let next = base[i % base.len()];
219 key.push(prev.wrapping_add(next).wrapping_mul(0x45).wrapping_add(0x12));
220 }
221 key
222 }
223
224 pub fn encrypt(data: &[u8], user_id: &str) -> Vec<u8> {
226 let key = Self::derive_key(user_id);
227 data.iter()
228 .enumerate()
229 .map(|(i, &b)| b ^ key[i % key.len()])
230 .collect()
231 }
232
233 pub fn decrypt(data: &[u8], user_id: &str) -> Result<Vec<u8>, String> {
235 Ok(Self::encrypt(data, user_id))
237 }
238}
239
240#[derive(Debug, Clone)]
246pub struct BackupEntry {
247 pub backup_idx: usize,
248 pub timestamp: u64,
249 pub size_bytes: usize,
250 pub checksum: u32,
251}
252
253#[derive(Debug, Default)]
255pub struct BackupManager {
256 backups: HashMap<u32, Vec<(u64, Vec<u8>)>>,
258}
259
260impl BackupManager {
261 const MAX_BACKUPS: usize = 5;
262
263 pub fn new() -> Self {
264 Self { backups: HashMap::new() }
265 }
266
267 pub fn backup(&mut self, slot_id: u32, data: Vec<u8>, timestamp: u64) {
269 let slot_backups = self.backups.entry(slot_id).or_default();
270 if slot_backups.len() >= Self::MAX_BACKUPS {
271 slot_backups.remove(0); }
273 slot_backups.push((timestamp, data));
274 }
275
276 pub fn restore_backup(&self, slot_id: u32, backup_idx: usize) -> Option<Vec<u8>> {
278 let slot_backups = self.backups.get(&slot_id)?;
279 slot_backups.get(backup_idx).map(|(_, data)| data.clone())
280 }
281
282 pub fn list_backups(&self, slot_id: u32) -> Vec<BackupEntry> {
284 match self.backups.get(&slot_id) {
285 None => Vec::new(),
286 Some(backups) => backups
287 .iter()
288 .enumerate()
289 .map(|(i, (ts, data))| BackupEntry {
290 backup_idx: i,
291 timestamp: *ts,
292 size_bytes: data.len(),
293 checksum: simple_checksum(data),
294 })
295 .collect(),
296 }
297 }
298
299 pub fn backup_count(&self, slot_id: u32) -> usize {
301 self.backups.get(&slot_id).map_or(0, |v| v.len())
302 }
303}
304
305pub struct CloudSaveClient {
312 pub provider: CloudProvider,
313 pub user_id: String,
314 pub state: CloudSyncState,
315 pub stats: CloudSaveStats,
316 pub metadata: CloudMetadata,
317 pub conflict_resolution: ConflictResolution,
318
319 remote_store: HashMap<u32, CloudSaveEntry>,
321 local_store: HashMap<u32, Vec<u8>>,
323
324 offline_queue: OfflineQueue,
325 backup_manager: BackupManager,
326 is_online: bool,
327}
328
329impl CloudSaveClient {
330 const MAX_RETRIES: u32 = 3;
331 const MAX_CONCURRENT_UPLOADS: usize = 2;
332
333 pub fn new(provider: CloudProvider, user_id: impl Into<String>) -> Self {
334 Self {
335 provider,
336 user_id: user_id.into(),
337 state: CloudSyncState::Idle,
338 stats: CloudSaveStats::default(),
339 metadata: CloudMetadata { quota_bytes: 100 * 1024 * 1024, ..Default::default() },
340 conflict_resolution: ConflictResolution::MergeByTimestamp,
341 remote_store: HashMap::new(),
342 local_store: HashMap::new(),
343 offline_queue: OfflineQueue::new(),
344 backup_manager: BackupManager::new(),
345 is_online: true,
346 }
347 }
348
349 pub fn set_online(&mut self, online: bool) {
350 self.is_online = online;
351 if online {
352 self.drain_offline_queue();
353 }
354 }
355
356 pub fn upload_slot(&mut self, slot_id: u32, data: Vec<u8>, timestamp: u64) -> Result<(), String> {
358 if !self.is_online {
359 self.offline_queue.push(PendingOp::Upload { slot_id, data, timestamp });
360 return Err("offline — operation queued".into());
361 }
362
363 self.backup_manager.backup(slot_id, data.clone(), timestamp);
364
365 let mut last_err = String::new();
366 for attempt in 0..Self::MAX_RETRIES {
367 match self.do_upload(slot_id, data.clone(), timestamp) {
368 Ok(()) => {
369 self.stats.record_upload(data.len());
370 self.state = CloudSyncState::Uploading(1.0);
371 return Ok(());
372 }
373 Err(e) => {
374 last_err = e;
375 let _backoff_ms = 100u64 * (1 << attempt);
376 }
378 }
379 }
380 self.state = CloudSyncState::Error(last_err.clone());
381 Err(last_err)
382 }
383
384 fn do_upload(&mut self, slot_id: u32, data: Vec<u8>, timestamp: u64) -> Result<(), String> {
385 let entry = CloudSaveEntry::new(slot_id, &self.user_id, data, timestamp);
386 self.metadata.used_bytes = self.metadata.used_bytes
387 .saturating_add(entry.size_bytes as u64);
388 self.remote_store.insert(slot_id, entry);
389 Ok(())
390 }
391
392 pub fn download_slot(&mut self, slot_id: u32) -> Result<Vec<u8>, String> {
394 if !self.is_online {
395 self.offline_queue.push(PendingOp::Download { slot_id });
396 return Err("offline — operation queued".into());
397 }
398
399 let entry = self.remote_store.get(&slot_id)
400 .ok_or_else(|| format!("slot {slot_id} not found in remote store"))?
401 .clone();
402
403 if !entry.verify_checksum() {
404 return Err(format!("slot {slot_id} checksum mismatch on download"));
405 }
406
407 self.stats.record_download(entry.size_bytes);
408 self.state = CloudSyncState::Downloading(1.0);
409 self.local_store.insert(slot_id, entry.save_data.clone());
410 Ok(entry.save_data)
411 }
412
413 pub fn resolve_conflict(
417 &mut self,
418 slot_id: u32,
419 local_data: Vec<u8>,
420 local_timestamp: u64,
421 resolution: ConflictResolution,
422 ) -> Result<Vec<u8>, String> {
423 let remote = self.remote_store.get(&slot_id).cloned();
424 let local_entry = CloudSaveEntry::new(slot_id, &self.user_id, local_data, local_timestamp);
425
426 match remote {
427 None => {
428 Ok(local_entry.save_data)
430 }
431 Some(remote_entry) => {
432 if local_entry.checksum == remote_entry.checksum {
433 return Ok(local_entry.save_data); }
435 self.stats.record_conflict_resolved();
436 let winner = match resolution {
437 ConflictResolution::LocalWins => local_entry.save_data,
438 ConflictResolution::RemoteWins => remote_entry.save_data,
439 ConflictResolution::MergeByTimestamp => {
440 if local_entry.timestamp >= remote_entry.timestamp {
441 local_entry.save_data
442 } else {
443 remote_entry.save_data
444 }
445 }
446 ConflictResolution::MergeByVersion => {
447 if local_entry.size_bytes >= remote_entry.size_bytes {
449 local_entry.save_data
450 } else {
451 remote_entry.save_data
452 }
453 }
454 ConflictResolution::UserChoice => {
455 local_entry.save_data
457 }
458 };
459 Ok(winner)
460 }
461 }
462 }
463
464 pub fn sync_all(&mut self, timestamp: u64) -> Result<(), String> {
466 let slots: Vec<(u32, Vec<u8>)> = self.local_store
467 .iter()
468 .map(|(&id, data)| (id, data.clone()))
469 .collect();
470
471 let mut errors = Vec::new();
472 let mut in_flight = 0usize;
473
474 for (slot_id, data) in slots {
475 if in_flight >= Self::MAX_CONCURRENT_UPLOADS {
476 in_flight = 0; }
478 if let Err(e) = self.upload_slot(slot_id, data, timestamp) {
479 errors.push(e);
480 } else {
481 in_flight += 1;
482 }
483 }
484
485 if errors.is_empty() {
486 self.metadata.last_sync_time = timestamp;
487 self.state = CloudSyncState::SyncComplete;
488 Ok(())
489 } else {
490 let msg = errors.join("; ");
491 self.state = CloudSyncState::Error(msg.clone());
492 Err(msg)
493 }
494 }
495
496 pub fn set_local(&mut self, slot_id: u32, data: Vec<u8>) {
498 self.local_store.insert(slot_id, data);
499 }
500
501 pub fn offline_queue(&self) -> &OfflineQueue {
502 &self.offline_queue
503 }
504
505 pub fn backup_manager(&self) -> &BackupManager {
506 &self.backup_manager
507 }
508
509 pub fn backup_manager_mut(&mut self) -> &mut BackupManager {
510 &mut self.backup_manager
511 }
512
513 fn drain_offline_queue(&mut self) {
514 let ops = self.offline_queue.drain();
515 for op in ops {
516 match op {
517 PendingOp::Upload { slot_id, data, timestamp } => {
518 let _ = self.do_upload(slot_id, data, timestamp);
519 }
520 PendingOp::Download { slot_id } => {
521 let _ = self.remote_store.get(&slot_id).cloned();
523 }
524 }
525 }
526 }
527}
528
529#[cfg(test)]
534mod tests {
535 use super::*;
536
537 fn make_client() -> CloudSaveClient {
538 CloudSaveClient::new(CloudProvider::SteamCloud, "user_123")
539 }
540
541 #[test]
542 fn test_upload_and_download_roundtrip() {
543 let mut client = make_client();
544 let data = b"save data content".to_vec();
545 client.upload_slot(0, data.clone(), 1000).unwrap();
546 let downloaded = client.download_slot(0).unwrap();
547 assert_eq!(downloaded, data);
548 }
549
550 #[test]
551 fn test_upload_creates_backup() {
552 let mut client = make_client();
553 let data = b"backup test".to_vec();
554 client.upload_slot(1, data.clone(), 2000).unwrap();
555 assert_eq!(client.backup_manager().backup_count(1), 1);
556 }
557
558 #[test]
559 fn test_offline_queue_enqueue() {
560 let mut client = make_client();
561 client.set_online(false);
562 let result = client.upload_slot(0, b"data".to_vec(), 100);
563 assert!(result.is_err());
564 assert_eq!(client.offline_queue().len(), 1);
565 }
566
567 #[test]
568 fn test_offline_queue_max_100() {
569 let mut queue = OfflineQueue::new();
570 for i in 0..105u64 {
571 queue.push(PendingOp::Upload { slot_id: i as u32, data: vec![], timestamp: i });
572 }
573 assert_eq!(queue.len(), 100);
574 }
575
576 #[test]
577 fn test_offline_queue_drains_on_reconnect() {
578 let mut client = make_client();
579 client.set_online(false);
580 let _ = client.upload_slot(5, b"queued".to_vec(), 500);
581 assert_eq!(client.offline_queue().len(), 1);
582 client.set_online(true);
583 assert_eq!(client.offline_queue().len(), 0);
584 }
585
586 #[test]
587 fn test_conflict_resolution_local_wins() {
588 let mut client = make_client();
589 let remote_data = b"remote version".to_vec();
590 client.upload_slot(0, remote_data.clone(), 100).unwrap();
591 let local_data = b"local version".to_vec();
592 let result = client.resolve_conflict(0, local_data.clone(), 200, ConflictResolution::LocalWins).unwrap();
593 assert_eq!(result, local_data);
594 }
595
596 #[test]
597 fn test_conflict_resolution_remote_wins() {
598 let mut client = make_client();
599 let remote_data = b"remote version".to_vec();
600 client.upload_slot(0, remote_data.clone(), 100).unwrap();
601 let local_data = b"local version".to_vec();
602 let result = client.resolve_conflict(0, local_data, 200, ConflictResolution::RemoteWins).unwrap();
603 assert_eq!(result, remote_data);
604 }
605
606 #[test]
607 fn test_conflict_resolution_merge_by_timestamp() {
608 let mut client = make_client();
609 client.upload_slot(0, b"old remote".to_vec(), 100).unwrap();
610 let local_data = b"newer local".to_vec();
611 let result = client.resolve_conflict(0, local_data.clone(), 999, ConflictResolution::MergeByTimestamp).unwrap();
612 assert_eq!(result, local_data);
613 }
614
615 #[test]
616 fn test_encryption_roundtrip() {
617 let data = b"sensitive save data 123".to_vec();
618 let encrypted = SaveEncryption::encrypt(&data, "user_abc");
619 assert_ne!(encrypted, data);
620 let decrypted = SaveEncryption::decrypt(&encrypted, "user_abc").unwrap();
621 assert_eq!(decrypted, data);
622 }
623
624 #[test]
625 fn test_encryption_different_users_differ() {
626 let data = b"hello world".to_vec();
627 let enc1 = SaveEncryption::encrypt(&data, "user_a");
628 let enc2 = SaveEncryption::encrypt(&data, "user_b");
629 assert_ne!(enc1, enc2);
630 }
631
632 #[test]
633 fn test_backup_manager_max_5() {
634 let mut bm = BackupManager::new();
635 for i in 0..7u64 {
636 bm.backup(0, vec![i as u8], i);
637 }
638 assert_eq!(bm.backup_count(0), 5);
639 }
640
641 #[test]
642 fn test_backup_manager_restore() {
643 let mut bm = BackupManager::new();
644 bm.backup(0, b"first".to_vec(), 1);
645 bm.backup(0, b"second".to_vec(), 2);
646 let restored = bm.restore_backup(0, 0).unwrap();
647 assert_eq!(restored, b"first");
648 }
649
650 #[test]
651 fn test_backup_manager_list() {
652 let mut bm = BackupManager::new();
653 bm.backup(0, b"v1".to_vec(), 100);
654 bm.backup(0, b"v2".to_vec(), 200);
655 let list = bm.list_backups(0);
656 assert_eq!(list.len(), 2);
657 assert_eq!(list[0].backup_idx, 0);
658 assert_eq!(list[1].backup_idx, 1);
659 }
660
661 #[test]
662 fn test_cloud_metadata_usage() {
663 let meta = CloudMetadata {
664 last_sync_time: 0,
665 total_slots: 10,
666 used_bytes: 50,
667 quota_bytes: 100,
668 };
669 assert!((meta.usage_fraction() - 0.5).abs() < 1e-6);
670 assert!(meta.has_space(40));
671 assert!(!meta.has_space(60));
672 }
673
674 #[test]
675 fn test_sync_all() {
676 let mut client = make_client();
677 client.set_local(0, b"slot0".to_vec());
678 client.set_local(1, b"slot1".to_vec());
679 client.sync_all(9999).unwrap();
680 assert!(matches!(client.state, CloudSyncState::SyncComplete));
681 }
682}