cuqueclicker_lib/save/
mod.rs1pub mod migrate;
17pub mod versions;
18
19use crate::game::state::GameState;
20
21pub const CURRENT_VERSION: u32 = 5;
24
25pub fn load_from_str(json: &str) -> GameState {
34 use versions::{v1, v2, v3, v4, v5};
35 match migrate::peek_version(json) {
36 1 => match serde_json::from_str::<v1::GameStateV1>(json) {
37 Ok(v) => v5::GameStateV5::from(v4::GameStateV4::from(v3::GameStateV3::from(
38 v2::GameStateV2::from(v),
39 )))
40 .into_current()
41 .migrate_runtime(),
42 Err(_) => GameState::default().migrate_runtime(),
43 },
44 2 => match serde_json::from_str::<v2::GameStateV2>(json) {
45 Ok(v) => v5::GameStateV5::from(v4::GameStateV4::from(v3::GameStateV3::from(v)))
46 .into_current()
47 .migrate_runtime(),
48 Err(_) => GameState::default().migrate_runtime(),
49 },
50 3 => match serde_json::from_str::<v3::GameStateV3>(json) {
51 Ok(v) => v5::GameStateV5::from(v4::GameStateV4::from(v))
52 .into_current()
53 .migrate_runtime(),
54 Err(_) => GameState::default().migrate_runtime(),
55 },
56 4 => match serde_json::from_str::<v4::GameStateV4>(json) {
57 Ok(v) => v5::GameStateV5::from(v).into_current().migrate_runtime(),
58 Err(_) => GameState::default().migrate_runtime(),
59 },
60 5 => match serde_json::from_str::<v5::GameStateV5>(json) {
61 Ok(v) => v.into_current().migrate_runtime(),
62 Err(_) => GameState::default().migrate_runtime(),
63 },
64 _ => GameState::default().migrate_runtime(),
65 }
66}
67
68pub fn save_to_string(state: &GameState) -> serde_json::Result<String> {
81 let mut sanitized = state.clone();
87 if sanitized.cuques.log10.is_nan() {
88 sanitized.cuques = crate::bignum::Mag::ZERO;
89 }
90 if sanitized.lifetime_cuques.log10.is_nan() {
91 sanitized.lifetime_cuques = crate::bignum::Mag::ZERO;
92 }
93 if sanitized.best_fps.log10.is_nan() {
94 sanitized.best_fps = crate::bignum::Mag::ZERO;
95 }
96 serde_json::to_string_pretty(&sanitized)
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 #[test]
104 fn pre_versioned_json_loads_through_v1() {
105 let legacy = r#"{
109 "cuques": 1234.5,
110 "total_clicks": 99,
111 "lifetime_cuques": 1234.5,
112 "best_fps": 0.0,
113 "golden_caught": 0,
114 "fingerers_owned": {"index_finger": 7},
115 "achievements_earned": ["first_finger"],
116 "upgrades_earned": ["click_mult_1"],
117 "prestige": 0,
118 "total_play_ticks": 0,
119 "buffs": []
120 }"#;
121 let s = load_from_str(legacy);
122 assert_eq!(s.version, CURRENT_VERSION);
123 assert_eq!(s.cuques, crate::bignum::Mag::from_f64(1234.5));
124 assert_eq!(s.total_clicks, 99);
125 assert_eq!(s.fingerer_count("index_finger"), 7);
126 assert!(s.has_achievement("first_finger"));
127 assert_eq!(s.tree.bought.len(), 1);
131 assert!(
132 s.tree
133 .bought
134 .contains(&crate::game::tree::coord::TreeCoord::ORIGIN)
135 );
136 }
137
138 #[test]
139 fn malformed_json_falls_back_to_default() {
140 let s = load_from_str("{ not valid json");
141 assert_eq!(s.cuques, crate::bignum::Mag::ZERO);
142 assert_eq!(s.version, CURRENT_VERSION);
143 }
144
145 #[test]
146 fn round_trip_through_save_to_string_preserves_state() {
147 let original = GameState {
148 cuques: crate::bignum::Mag::from_f64(4242.0),
149 total_clicks: 17,
150 ..GameState::default()
151 };
152 let json = save_to_string(&original).expect("serialize");
153 let loaded = load_from_str(&json);
154 assert_eq!(loaded.cuques, crate::bignum::Mag::from_f64(4242.0));
155 assert_eq!(loaded.total_clicks, 17);
156 assert_eq!(loaded.version, CURRENT_VERSION);
157 }
158
159 #[test]
160 fn round_trip_preserves_huge_mag_values_past_f64_range() {
161 let original = GameState {
168 cuques: crate::bignum::Mag { log10: 600.0 },
169 lifetime_cuques: crate::bignum::Mag { log10: 1200.0 },
170 best_fps: crate::bignum::Mag { log10: 750.0 },
171 ..GameState::default()
172 };
173 let json = save_to_string(&original).expect("serialize");
174 let loaded = load_from_str(&json);
175 assert!((loaded.cuques.log10 - 600.0).abs() < 1e-9);
176 assert!((loaded.lifetime_cuques.log10 - 1200.0).abs() < 1e-9);
177 assert!((loaded.best_fps.log10 - 750.0).abs() < 1e-9);
178 }
179
180 #[test]
181 fn v4_json_with_plain_number_fields_still_loads_under_v5() {
182 let v4_json = r#"{
186 "version": 4,
187 "cuques": 1234.5,
188 "total_clicks": 88,
189 "lifetime_cuques": 6789.0,
190 "best_fps": 12.0,
191 "golden_caught": 0,
192 "lucky_caught": 0,
193 "frenzy_caught": 0,
194 "buff_caught": 0,
195 "green_coin_caught": 0,
196 "fingerers_state": {},
197 "achievements_earned": [],
198 "prestige": 2,
199 "total_play_ticks": 4242,
200 "buffs": [],
201 "tree": { "bought": [], "cursor": {"x": 0, "y": 0}, "last_bought": null }
202 }"#;
203 let loaded = load_from_str(v4_json);
204 assert_eq!(loaded.version, CURRENT_VERSION);
205 assert!((loaded.cuques.to_f64() - 1234.5).abs() < 1e-9);
206 assert!((loaded.lifetime_cuques.to_f64() - 6789.0).abs() < 1e-9);
207 assert_eq!(loaded.total_clicks, 88);
208 assert_eq!(loaded.prestige, 2);
209 }
210}