Skip to content

Commit 7f0ebac

Browse files
committed
feat: Add ability to 'hot'-store savefile with Ctrl+Alt+S, amend readme, write changelog
1 parent 8f0690d commit 7f0ebac

18 files changed

Lines changed: 273 additions & 40 deletions

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111
-
1212

1313

14+
## [3.1.0] - 2026-04-17
15+
16+
### Added
17+
- 'Classic' tetromino randomizer (=uniform random + 1 reroll to avoid last piece).
18+
- 'Elektronika 60' gameplay preset (slightly scuffed but that's just how it is).
19+
- 'Blank slate' keybinds preset.
20+
- `Home`/`End` can now be used to skip to beginning/end of input history (in 'Start New Game'⇝'Game save').
21+
- `Alt+Enter` can now be used to view the a game save as replay (in 'Start New Game'⇝'Game save').
22+
- `Ctrl+Alt+S` can now be used to do a savefile 'store' from every menu now.
23+
* Respects save preferences i.e. deletes existing savefile if 'keep save file' is turned off.
24+
* Load and Store errors are printed in Advanced Settings.
25+
26+
### Changed
27+
- The savefile is now serialized using less verbose 'Rust Object Notation' (ron instead of json).
28+
* It is much more compact, still human-readable and solves a savefile corruption issue (see Fixed).
29+
30+
### Fixed
31+
- Fixed unloadable savefile due to floating point `inf`inity not being representable in strict json.
32+
- Make experimental input mode toggle during replays work (`Alt+I` instead of `Ctrl+I`).
33+
- Allow hot re-loading from savefile (`Ctrl+Alt+L`) from every menu now.
34+
35+
1436
## [3.0.0] - 2026-04-16
1537

1638
### Added

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ yay -S tetro-tui
209209
> | `Esc`, `q`, `Back`, | Go back |
210210
> | `Del`, `d` | Delete/Reset |
211211
> | `Ctrl`+`Alt`+`L` | Re-load from savefile (Caution: discards current application data!) |
212+
> | `Ctrl`+`Alt`+`S` | Do savefile 'store' (respects save preferences i.e. deletes if 'keep save file' is off) |
212213
> | `Ctrl`+`C` | Exit application (respects save preferences) |
213214
>
214215
> Specific to 'Scores and Replays':
@@ -264,6 +265,7 @@ yay -S tetro-tui
264265
> | `Ctrl`(+`Alt`)+`G` | Cycle through graphics settings slots |
265266
> | `Ctrl`+`Alt`+`B` | Toggle on/off visibility of tiles ('Blindfolded') |
266267
> | `Ctrl`+`Alt`+`L` | Re-load from savefile (Caution: discards current application data!) |
268+
> | `Ctrl`+`Alt`+`S` | Do savefile 'store' (respects save preferences i.e. deletes if 'keep save file' is off) |
267269
> | `Ctrl`+`C` | Exit application (respects save preferences) |
268270
>
269271
> </details>
@@ -296,6 +298,7 @@ yay -S tetro-tui
296298
> | `Alt`+`I` | (Experimental) Toggle instantaneous interactive input intervention mode |
297299
> | `Ctrl`(+`Alt`)+`G` | Cycle through Graphics Settings slots |
298300
> | `Ctrl`+`Alt`+`L` | Re-load from savefile (Caution: discards current application data!) |
301+
> | `Ctrl`+`Alt`+`S` | Do savefile 'store' (respects save preferences i.e. deletes if 'keep save file' is off) |
299302
> | `Ctrl`+`C` | Exit application (respects save preferences) |
300303
>
301304
> </details>

src/main.rs

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ use falling_tetromino_engine::{
2222
ExtDuration, GameEndCause, InGameTime, Notification, NotificationFeed, Stat, Tetromino,
2323
};
2424

25+
use crate::savefile_logic::SavefileResult;
2526
use crate::{
2627
game_mode_presets::GameModePreset,
2728
game_restoration::{
@@ -388,7 +389,8 @@ pub struct TemporaryAppData {
388389
pub renderer_used: usize,
389390
pub save_on_exit: SavefileGranularity,
390391
pub savefile_path: PathBuf, // This should technically be the same for a given compiled binary, but we compute it at runtime.
391-
pub loadfile_result: savefile_logic::SavefileResult<()>,
392+
pub loadfile_result: SavefileResult<()>,
393+
pub storefile_result: SavefileResult<()>,
392394
}
393395

394396
#[derive(Debug)]
@@ -413,21 +415,8 @@ impl<T: Write> Drop for Application<T> {
413415
// (Try to) undo terminal setup. Ignore errors cuz atp it's too late to take any flak from Crossterm.
414416
let _ = self.deinitialize_terminal_state();
415417

416-
if self.temp_data.save_on_exit != SavefileGranularity::NoSavefile {
417-
// If the user wants any of their data stored, try to do so.
418-
if let Err(e) = self.store_to_savefile() {
419-
eprintln!("Error storing savefile: {e}");
420-
}
421-
} else if self
422-
.temp_data
423-
.savefile_path
424-
.try_exists()
425-
.is_ok_and(|exists| exists)
426-
{
427-
// Otherwise explicitly check for savefile and try to make sure we don't leave it around.
428-
if let Err(e) = std::fs::remove_file(self.temp_data.savefile_path.clone()) {
429-
eprintln!("Error removing old savefile: {e}");
430-
}
418+
if let Err(e) = self.savefile_store() {
419+
eprintln!("Error on savefile store: {e}");
431420
}
432421
}
433422
}
@@ -562,6 +551,7 @@ impl<T: Write> Application<T> {
562551
save_on_exit: SavefileGranularity::default(),
563552
savefile_path: savefile_logic::savefile_path(),
564553
loadfile_result: Ok(()),
554+
storefile_result: Ok(()),
565555
};
566556

567557
let mut new = Self {
@@ -574,7 +564,7 @@ impl<T: Write> Application<T> {
574564
};
575565

576566
// Load in actual settings.
577-
new.temp_data.loadfile_result = new.load_from_savefile();
567+
new.temp_data.loadfile_result = new.savefile_load();
578568

579569
// Special: Overwrite specifically requested cmdline flags.
580570

src/savefile_logic.rs

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -91,15 +91,35 @@ struct SaveContents<'a> {
9191
}
9292

9393
impl<T: Write> Application<T> {
94-
pub fn store_to_savefile(&mut self) -> SavefileResult<()> {
95-
if self.temp_data.save_on_exit < SavefileGranularity::StoreSettingsScores {
96-
// Clear scoreboard if no game data is wished to be stored.
97-
self.scores_and_replays.entries.clear();
98-
} else if self.temp_data.save_on_exit < SavefileGranularity::StoreSettingsScoresReplays {
94+
pub fn savefile_store(&mut self) -> SavefileResult<()> {
95+
match self.temp_data.save_on_exit {
96+
// Explicitly check for savefile and try to make sure we don't leave it around.
97+
SavefileGranularity::NoSavefile => {
98+
if self
99+
.temp_data
100+
.savefile_path
101+
.try_exists()
102+
.is_ok_and(|exists| exists)
103+
{
104+
std::fs::remove_file(self.temp_data.savefile_path.clone())?;
105+
return Ok(());
106+
}
107+
}
108+
109+
// Clear scoreboard if no data other than settings is wished to be stored.
110+
SavefileGranularity::StoreSettings => {
111+
self.scores_and_replays.entries.clear();
112+
}
113+
99114
// Clear past game restoration data if no game replay data is wished to be stored.
100-
for (_entry, restoration_data) in &mut self.scores_and_replays.entries {
101-
restoration_data.take();
115+
SavefileGranularity::StoreSettingsScores => {
116+
for (_entry, restoration_data) in &mut self.scores_and_replays.entries {
117+
restoration_data.take();
118+
}
102119
}
120+
121+
// Everything to be stored. Fall through.
122+
SavefileGranularity::StoreSettingsScoresReplays => {}
103123
}
104124

105125
let save_contents = SaveContents {
@@ -133,7 +153,7 @@ impl<T: Write> Application<T> {
133153
}
134154
}
135155

136-
pub fn load_from_savefile(&mut self) -> SavefileResult<()> {
156+
pub fn savefile_load(&mut self) -> SavefileResult<()> {
137157
let mut file = File::open(self.temp_data.savefile_path.clone())?;
138158
let mut save_str = String::new();
139159
file.read_to_string(&mut save_str)?;

src/tui_menus/about.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,17 @@ impl<T: Write> Application<T> {
9595
kind: Press | Repeat,
9696
..
9797
}) if { modifiers.contains(KeyModifiers::CONTROL.union(KeyModifiers::ALT)) } => {
98-
self.temp_data.loadfile_result = self.load_from_savefile();
98+
self.temp_data.loadfile_result = self.savefile_load();
99+
}
100+
101+
// Store to savefile.
102+
Event::Key(KeyEvent {
103+
code: KeyCode::Char('s' | 'S'),
104+
modifiers,
105+
kind: Press | Repeat,
106+
..
107+
}) if { modifiers.contains(KeyModifiers::CONTROL.union(KeyModifiers::ALT)) } => {
108+
self.temp_data.storefile_result = self.savefile_store();
99109
}
100110

101111
// Other event: don't care.

src/tui_menus/adjust_gameplay.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,8 +264,19 @@ impl<T: Write> Application<T> {
264264
kind: Press | Repeat,
265265
..
266266
}) if { modifiers.contains(KeyModifiers::CONTROL.union(KeyModifiers::ALT)) } => {
267-
self.temp_data.loadfile_result = self.load_from_savefile();
267+
self.temp_data.loadfile_result = self.savefile_load();
268268
}
269+
270+
// Store to savefile.
271+
Event::Key(KeyEvent {
272+
code: KeyCode::Char('s' | 'S'),
273+
modifiers,
274+
kind: Press | Repeat,
275+
..
276+
}) if { modifiers.contains(KeyModifiers::CONTROL.union(KeyModifiers::ALT)) } => {
277+
self.temp_data.storefile_result = self.savefile_store();
278+
}
279+
269280
Event::Key(KeyEvent {
270281
code: KeyCode::Right | KeyCode::Char('l' | 'L'),
271282
kind: Press | Repeat,

src/tui_menus/adjust_graphics.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,17 @@ impl<T: Write> Application<T> {
320320
kind: Press | Repeat,
321321
..
322322
}) if { modifiers.contains(KeyModifiers::CONTROL.union(KeyModifiers::ALT)) } => {
323-
self.temp_data.loadfile_result = self.load_from_savefile();
323+
self.temp_data.loadfile_result = self.savefile_load();
324+
}
325+
326+
// Store to savefile.
327+
Event::Key(KeyEvent {
328+
code: KeyCode::Char('s' | 'S'),
329+
modifiers,
330+
kind: Press | Repeat,
331+
..
332+
}) if { modifiers.contains(KeyModifiers::CONTROL.union(KeyModifiers::ALT)) } => {
333+
self.temp_data.storefile_result = self.savefile_store();
324334
}
325335

326336
Event::Key(KeyEvent {

src/tui_menus/adjust_keybinds.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,17 @@ impl<T: Write> Application<T> {
268268
kind: Press | Repeat,
269269
..
270270
}) if { modifiers.contains(KeyModifiers::CONTROL.union(KeyModifiers::ALT)) } => {
271-
self.temp_data.loadfile_result = self.load_from_savefile();
271+
self.temp_data.loadfile_result = self.savefile_load();
272+
}
273+
274+
// Store to savefile.
275+
Event::Key(KeyEvent {
276+
code: KeyCode::Char('s' | 'S'),
277+
modifiers,
278+
kind: Press | Repeat,
279+
..
280+
}) if { modifiers.contains(KeyModifiers::CONTROL.union(KeyModifiers::ALT)) } => {
281+
self.temp_data.storefile_result = self.savefile_store();
272282
}
273283

274284
// Cycle slot to right.

src/tui_menus/advanced_settings.rs

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,41 @@ impl<T: Write> Application<T> {
134134
.queue(PrintStyledContent(
135135
format!(
136136
"{:^w_main$}",
137-
format!("Trying to load savefile on start caused this error:")
137+
format!("Latest error from trying to load savefile:")
138+
)
139+
.italic(),
140+
))?
141+
.queue(MoveTo(
142+
x_main,
143+
y_main
144+
+ y_selection
145+
+ 4
146+
+ u16::try_from(selection_len).unwrap()
147+
+ 3
148+
+ temp_offset
149+
+ 1,
150+
))?
151+
.queue(PrintStyledContent(
152+
format!("{:^w_main$}", format!("{e}")).italic(),
153+
))?;
154+
temp_offset += 1;
155+
}
156+
157+
if let Err(e) = &self.temp_data.storefile_result {
158+
self.term
159+
.queue(MoveTo(
160+
x_main,
161+
y_main
162+
+ y_selection
163+
+ 4
164+
+ u16::try_from(selection_len).unwrap()
165+
+ 3
166+
+ temp_offset,
167+
))?
168+
.queue(PrintStyledContent(
169+
format!(
170+
"{:^w_main$}",
171+
format!("Latest error from trying to store savefile:")
138172
)
139173
.italic(),
140174
))?
@@ -228,7 +262,17 @@ impl<T: Write> Application<T> {
228262
kind: Press | Repeat,
229263
..
230264
}) if { modifiers.contains(KeyModifiers::CONTROL.union(KeyModifiers::ALT)) } => {
231-
self.temp_data.loadfile_result = self.load_from_savefile();
265+
self.temp_data.loadfile_result = self.savefile_load();
266+
}
267+
268+
// Store to savefile.
269+
Event::Key(KeyEvent {
270+
code: KeyCode::Char('s' | 'S'),
271+
modifiers,
272+
kind: Press | Repeat,
273+
..
274+
}) if { modifiers.contains(KeyModifiers::CONTROL.union(KeyModifiers::ALT)) } => {
275+
self.temp_data.storefile_result = self.savefile_store();
232276
}
233277

234278
Event::Key(KeyEvent {

src/tui_menus/game_ended.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,17 @@ impl<T: Write> Application<T> {
254254
kind: Press | Repeat,
255255
..
256256
}) if { modifiers.contains(KeyModifiers::CONTROL.union(KeyModifiers::ALT)) } => {
257-
self.temp_data.loadfile_result = self.load_from_savefile();
257+
self.temp_data.loadfile_result = self.savefile_load();
258+
}
259+
260+
// Store to savefile.
261+
Event::Key(KeyEvent {
262+
code: KeyCode::Char('s' | 'S'),
263+
modifiers,
264+
kind: Press | Repeat,
265+
..
266+
}) if { modifiers.contains(KeyModifiers::CONTROL.union(KeyModifiers::ALT)) } => {
267+
self.temp_data.storefile_result = self.savefile_store();
258268
}
259269

260270
Event::Resize(..) => {}

0 commit comments

Comments
 (0)