lsdj/sram/mod.rs
1//! LittleSoundDJ SRAM/`.sav` file handling
2//!
3//! This module contains code for manipulating [`SRam`], which is where LSDJ stores our
4//! songs (compressed and uncompressed). Usually people work with `.sav` files, which gameboy
5//! emulators use to store the SRAM tied to a ROM. You can also download/upload `.sav`
6//! files to flashcarts for playback on real hardware.
7
8use crate::{
9 fs::{self, Filesystem},
10 song::{self, SongMemory},
11};
12use std::{
13 fs::{File, create_dir_all},
14 io::{self, Read, Write},
15 path::Path,
16};
17use thiserror::Error;
18
19/// A full representation of LittleSoundDJ SRAM
20///
21/// Every LSDJ save file consists of the same amount of bytes, in which both the song you're
22/// currently working on is stored (uncompressed), as well as a filesystem containing at max
23/// 32 (compressed) songs.
24///
25/// The first time you boot LSDJ it formats the SRAM to the expected structure, setting some
26/// magic bytes for later verification as well. This crate allows you to do the same, but also
27/// to deserialize [`SRam`] from disk or an arbitrary reader.
28///
29/// ```no_run
30/// # use lsdj::sram::SRam;
31/// # use std::fs::File;
32/// // Construct valid SRAM with the default/empty song and an empty filesystem
33/// let sram = SRam::new();
34///
35/// // Load SRAM from a path on disk
36/// let sram = SRam::from_path("bangers.sav")?;
37///
38/// // Load SRAM from an arbitrary reader
39/// let sram = SRam::from_reader(File::open("bangers.sav")?)?;
40/// # Ok::<(), anyhow::Error>(())
41/// ```
42///
43/// In the same way, SRAM can be serialized back to the underlying byte structure:
44///
45/// ```no_run
46/// # use lsdj::sram::SRam;
47/// # use std::fs::File;
48/// # let sram = SRam::new();
49/// // Load SRAM from a path on disk
50/// sram.to_path("bangers.sav")?;
51///
52/// // Load SRAM from an arbitrary reader
53/// sram.to_writer(File::create("bangers.sav")?)?;
54/// # Ok::<(), std::io::Error>(())
55/// ```
56pub struct SRam {
57 /// The song that's currently being worked on in LSDJ
58 pub working_memory_song: SongMemory,
59
60 /// Compressed storage for songs not currently worked on
61 pub filesystem: Filesystem,
62}
63
64impl SRam {
65 /// Construct a new SRAM, with a default song and empty filesystem
66 ///
67 /// This function also sets some necessary verification bytes which LSDJ uses to check
68 /// for corrupted memory
69 pub fn new() -> Self {
70 Self {
71 working_memory_song: SongMemory::new(),
72 filesystem: Filesystem::new(),
73 }
74 }
75
76 /// Deserialize SRAM from an arbitrary I/O reader
77 pub fn from_reader<R>(mut reader: R) -> Result<Self, FromReaderError>
78 where
79 R: Read,
80 {
81 let working_memory_song = SongMemory::from_reader(&mut reader)?;
82 let filesystem = Filesystem::from_reader(&mut reader)?;
83
84 Ok(Self {
85 working_memory_song,
86 filesystem,
87 })
88 }
89
90 /// Deserialize SRAM from a path on disk (.sav)
91 pub fn from_path<P>(path: P) -> Result<Self, FromPathError>
92 where
93 P: AsRef<Path>,
94 {
95 let file = File::open(path)?;
96 let sram = Self::from_reader(file)?;
97
98 Ok(sram)
99 }
100
101 /// Serialize SRAM to an arbitrary I/O writer
102 pub fn to_writer<W>(&self, mut writer: W) -> Result<(), io::Error>
103 where
104 W: Write,
105 {
106 self.working_memory_song.to_writer(&mut writer)?;
107 self.filesystem.to_writer(writer)
108 }
109
110 /// Serialize SRAM to a path on disk (.sav)
111 pub fn to_path<P>(&self, path: P) -> Result<(), io::Error>
112 where
113 P: AsRef<Path>,
114 {
115 let path = path.as_ref();
116 create_dir_all(path.parent().unwrap())?;
117 self.to_writer(File::create(path)?)
118 }
119}
120
121impl Default for SRam {
122 fn default() -> Self {
123 Self::new()
124 }
125}
126
127/// Errors that might be returned from [`SRam::from_reader()`]
128#[derive(Debug, Error)]
129pub enum FromReaderError {
130 /// Deserializing the working memory song from I/O failed
131 #[error("Reading the working memory song failed")]
132 WorkingSong(#[from] song::FromReaderError),
133
134 /// Deserializing the file system from I/O failed
135 #[error("Reading the filesystem failed")]
136 Filesystem(#[from] fs::FromReaderError),
137}
138
139/// Errors that might be returned from [`SRam::from_path()`]
140#[derive(Debug, Error)]
141pub enum FromPathError {
142 /// Opening the file itself failed
143 #[error("Opening the file failed")]
144 FileOpen(#[from] io::Error),
145
146 /// Deserialization itself somehow failed
147 #[error("Reading the SRAM from file failed")]
148 Read(#[from] FromReaderError),
149}