pub struct Storage { /* private fields */ }Expand description
Key-value storage that survives restarts, optionally encrypted.
Implementations§
Source§impl Storage
impl Storage
Sourcepub fn open(path: impl Into<PathBuf>) -> Self
pub fn open(path: impl Into<PathBuf>) -> Self
Loads storage from a plain text file at path. A missing file simply gives empty
storage. An encrypted file cannot be read without its secret: it is renamed to
<file>.broken and Storage::load_error explains why.
Sourcepub fn open_encrypted(path: impl Into<PathBuf>, secret: &str) -> Self
pub fn open_encrypted(path: impl Into<PathBuf>, secret: &str) -> Self
Loads storage from an encrypted file at path, using secret to make the key. A
missing file gives empty storage. A plain text file is read and written encrypted on
the next save. A file that cannot be decrypted (wrong secret, edited or damaged) is
renamed to <file>.broken, and Storage::load_error explains why.
Sourcepub fn load_error(&self) -> Option<&str>
pub fn load_error(&self) -> Option<&str>
Why the save file could not be loaded, if it could not: wrong secret, edited or
damaged file. The unreadable file was kept as <file>.broken. None when loading
worked or there was no file.
if ctx.storage.load_error().is_some() {
ctx.speak("Your save file is damaged. Starting a new game.");
}Sourcepub fn is_encrypted(&self) -> bool
pub fn is_encrypted(&self) -> bool
Whether the file is written encrypted.
Sourcepub fn set_encryption(&mut self, secret: Option<&str>)
pub fn set_encryption(&mut self, secret: Option<&str>)
Turns encryption on with a secret (Some) or off (None). The file is rewritten in
the new form on the next save.
Sourcepub fn get<T: FromStr>(&self, key: &str) -> Option<T>
pub fn get<T: FromStr>(&self, key: &str) -> Option<T>
Reads a value. Returns None if it is missing or cannot be parsed as T.
Examples found in repository?
22fn main() -> audio_code::Result<()> {
23 Engine::new("Bell Catcher").run_with(|ctx| {
24 ctx.audio.register("bell", Synth::sine(1568.0).with(Synth::sine(3136.0).volume(0.3)).length(0.35).decay(9.0).volume(0.6));
25 ctx.audio.register("step", Synth::noise().length(0.05).decay(80.0).volume(0.5));
26 ctx.audio.register("edge", Synth::sine(110.0).length(0.12).decay(30.0).volume(0.8));
27 ctx.audio.register(
28 "catch",
29 Synth::sine(784.0).length(0.08).then(Synth::sine(1047.0).length(0.08)).then(Synth::sine(1568.0).length(0.2).decay(12.0)).volume(0.4),
30 );
31 ctx.audio.register("miss", Synth::saw(220.0).sweep_to(80.0).length(0.5).decay(5.0).volume(0.35));
32
33 // Restore the speech settings from the last session.
34 if let Some(voice) = ctx.storage.get::<String>("voice") {
35 ctx.speech.set_voice(voice);
36 }
37 let rate: f32 = ctx.storage.get_or("rate", 50.0);
38 if rate != 50.0 {
39 ctx.speech.set_rate(rate / 100.0);
40 }
41 Title
42 })
43}Sourcepub fn get_or<T: FromStr>(&self, key: &str, default: T) -> T
pub fn get_or<T: FromStr>(&self, key: &str, default: T) -> T
Reads a value, or default if it is missing or invalid.
Examples found in repository?
120 fn finish(&mut self, ctx: &mut Context) {
121 let seconds = self.clock.elapsed_secs().round() as u32;
122 let best: u32 = ctx.storage.get_or("best_time", u32::MAX);
123 let mut text = format!("You found every beacon in {seconds} seconds.");
124 if seconds < best {
125 ctx.storage.set("best_time", seconds);
126 text.push_str(" That is a new record!");
127 } else {
128 text.push_str(&format!(" Your record is {best} seconds."));
129 }
130 ctx.ui.message("done", text);
131 }More examples
8fn main() -> audio_code::Result<()> {
9 let mut score = 0;
10
11 audio_code::run("Clicker", move |ctx| {
12 if ctx.once("intro") {
13 let best: u32 = ctx.storage.get_or("best", 0);
14 ctx.speak(format!("Press space as often as you can in ten seconds. Your best is {best}. Go!"));
15 ctx.time.after("end", 10.0);
16 }
17
18 if ctx.time.is_scheduled("end") && ctx.pressed(Key::Space) {
19 score += 1;
20 ctx.audio.play_tone(300.0 + score as f32 * 15.0, 0.05);
21 }
22
23 if ctx.time.fired("end") {
24 let best: u32 = ctx.storage.get_or("best", 0);
25 if score > best {
26 ctx.storage.set("best", score);
27 ctx.ui.message("result", format!("{score} presses. New record!"));
28 } else {
29 ctx.ui.message("result", format!("{score} presses. Your best is {best}."));
30 }
31 }
32
33 if ctx.ui.closed("result") || ctx.pressed(Key::Escape) {
34 ctx.quit();
35 }
36 })
37}22fn main() -> audio_code::Result<()> {
23 Engine::new("Bell Catcher").run_with(|ctx| {
24 ctx.audio.register("bell", Synth::sine(1568.0).with(Synth::sine(3136.0).volume(0.3)).length(0.35).decay(9.0).volume(0.6));
25 ctx.audio.register("step", Synth::noise().length(0.05).decay(80.0).volume(0.5));
26 ctx.audio.register("edge", Synth::sine(110.0).length(0.12).decay(30.0).volume(0.8));
27 ctx.audio.register(
28 "catch",
29 Synth::sine(784.0).length(0.08).then(Synth::sine(1047.0).length(0.08)).then(Synth::sine(1568.0).length(0.2).decay(12.0)).volume(0.4),
30 );
31 ctx.audio.register("miss", Synth::saw(220.0).sweep_to(80.0).length(0.5).decay(5.0).volume(0.35));
32
33 // Restore the speech settings from the last session.
34 if let Some(voice) = ctx.storage.get::<String>("voice") {
35 ctx.speech.set_voice(voice);
36 }
37 let rate: f32 = ctx.storage.get_or("rate", 50.0);
38 if rate != 50.0 {
39 ctx.speech.set_rate(rate / 100.0);
40 }
41 Title
42 })
43}
44
45// ------------------------------------------------------------------------- menu
46
47struct Title;
48
49fn title_menu(ctx: &Context) -> Menu {
50 let voice = ctx.speech.voice().map_or("default".to_string(), |v| v.name.clone());
51 let best: u32 = ctx.storage.get_or("best", 0);
52 Menu::new("title", "Bell Catcher")
53 .button("play", "Play")
54 .button("voice", format!("Voice: {voice}"))
55 .slider("rate", "Speech rate", ctx.storage.get_or("rate", 50.0), 0.0, 100.0, 5.0)
56 .button("help", "How to play")
57 .label(format!("Best score: {best}"))
58 .button("quit", "Quit")
59 .closable(false)
60 .wrap(true)
61}
62
63/// One button per voice, with the current one focused. `None` if the output has no voices.
64fn voice_menu(ctx: &Context) -> Option<Menu> {
65 let voices = ctx.speech.voices();
66 if voices.is_empty() {
67 return None;
68 }
69 let mut menu = Menu::new("voices", "Choose a voice").button("default", "Default voice");
70 for (i, voice) in voices.iter().enumerate() {
71 menu = menu.button(format!("voice{i}"), voice.to_string());
72 }
73 if let Some(i) = ctx.speech.voice().and_then(|current| voices.iter().position(|v| v == current)) {
74 menu = menu.focus(&format!("voice{i}"));
75 }
76 Some(menu.button("back", "Back"))
77}
78
79/// Reopens the title menu so its labels show the new values, focused on `item`.
80fn refresh_title(ctx: &mut Context, item: &str) {
81 ctx.ui.close_id("title");
82 let menu = title_menu(ctx).focus(item);
83 ctx.ui.open(menu);
84}
85
86impl Scene for Title {
87 fn start(&mut self, ctx: &mut Context) {
88 let menu = title_menu(ctx);
89 ctx.ui.open(menu);
90 }
91
92 fn on_ui(&mut self, event: &UiEvent, ctx: &mut Context) {
93 match event {
94 UiEvent::Activated { menu, item } if menu == "title" => match item.as_str() {
95 "play" => {
96 ctx.ui.close_all();
97 ctx.switch_scene(Game::default());
98 }
99 "voice" => match voice_menu(ctx) {
100 Some(menu) => ctx.ui.open(menu),
101 None => ctx.ui.message(
102 "no voices",
103 "Your screen reader speaks with its own voice. Change it in the screen reader's settings.",
104 ),
105 },
106 "help" => ctx.ui.message(
107 "help",
108 "Bells fall in one of seven lanes in front of you. Listen where a bell rings, \
109 step under it with left and right, and catch it before it lands. \
110 The bell rings faster and higher the closer it gets. You have three lives. \
111 Press S for your score, Escape for the menu.",
112 ),
113 "quit" => ctx.quit(),
114 _ => {}
115 },
116 UiEvent::Activated { menu, item } if menu == "voices" => {
117 if item == "back" {
118 ctx.ui.close();
119 return;
120 }
121 if item == "default" {
122 ctx.speech.reset_voice();
123 ctx.storage.remove("voice");
124 } else if let Some(voice) =
125 item.strip_prefix("voice").and_then(|i| i.parse::<usize>().ok()).and_then(|i| ctx.speech.voices().get(i).cloned())
126 && ctx.speech.set_voice(&voice.id)
127 {
128 ctx.storage.set("voice", &voice.id);
129 }
130 ctx.ui.close();
131 refresh_title(ctx, "voice");
132 let name = ctx.speech.voice().map_or("Default voice".to_string(), |v| v.name.clone());
133 ctx.speech.speak_queued(format!("{name}. This is how I sound now."));
134 }
135 UiEvent::Changed { menu, item, value } if menu == "title" && item == "rate" => {
136 ctx.speech.set_rate(value.as_f32() / 100.0);
137 ctx.storage.set("rate", value.as_f32());
138 }
139 _ => {}
140 }
141 }
142
143 fn update(&mut self, _ctx: &mut Context) {}
144}
145
146// ------------------------------------------------------------------------- game
147
148struct Game {
149 lane: i32,
150 bell_lane: i32,
151 /// Seconds a bell needs to land; shrinks with every catch.
152 fall: f32,
153 /// Seconds until the current bell lands. Negative while waiting for the next bell.
154 left: f32,
155 next_ring: f32,
156 score: u32,
157 lives: u32,
158 over: bool,
159}
160
161impl Default for Game {
162 fn default() -> Self {
163 Self { lane: 0, bell_lane: 0, fall: FIRST_FALL, left: 0.0, next_ring: 0.0, score: 0, lives: LIVES, over: false }
164 }
165}
166
167impl Game {
168 fn drop_bell(&mut self) {
169 self.bell_lane = random::int(-LANES as i64, LANES as i64) as i32;
170 self.left = self.fall;
171 self.next_ring = 0.0;
172 }
173
174 /// Where the bell is, seen from the player: lanes to the side, a little in front.
175 fn bell_position(&self) -> Vec3 {
176 vec3((self.bell_lane - self.lane) as f32 * LANE_WIDTH, 2.0, 0.0)
177 }
178
179 fn ring(&mut self, ctx: &mut Context) {
180 // 0.0 when the bell starts falling, 1.0 when it lands.
181 let progress = 1.0 - self.left / self.fall;
182 let position = self.bell_position();
183 ctx.audio.sound("bell").at(position).pitch(1.0 + progress * 0.6).play();
184 self.next_ring = 0.12 + 0.45 * (1.0 - progress);
185 }
186
187 fn land(&mut self, ctx: &mut Context) {
188 if self.lane == self.bell_lane {
189 self.score += 1;
190 self.fall = (self.fall * 0.93).max(FASTEST_FALL);
191 ctx.play("catch");
192 if self.score.is_multiple_of(5) {
193 ctx.speak(format!("{} bells!", self.score));
194 }
195 } else {
196 self.lives -= 1;
197 ctx.play("miss");
198 match self.lives {
199 0 => return self.game_over(ctx),
200 1 => ctx.speak("Missed! Last life."),
201 n => ctx.speak(format!("Missed! {n} lives left.")),
202 }
203 }
204 // A short break before the next bell.
205 self.left = -0.8;
206 }
207
208 fn game_over(&mut self, ctx: &mut Context) {
209 self.over = true;
210 let best: u32 = ctx.storage.get_or("best", 0);
211 let text = if self.score > best {
212 ctx.storage.set("best", self.score);
213 format!("Game over. {} bells, a new best score!", self.score)
214 } else {
215 format!("Game over. {} bells. Your best is {best}.", self.score)
216 };
217 ctx.ui.message("over", text);
218 }Sourcepub fn set(&mut self, key: &str, value: impl ToString)
pub fn set(&mut self, key: &str, value: impl ToString)
Stores a value. It is written to disk when the game quits or Storage::save is called.
§Panics
If key contains = or a line break.
Examples found in repository?
120 fn finish(&mut self, ctx: &mut Context) {
121 let seconds = self.clock.elapsed_secs().round() as u32;
122 let best: u32 = ctx.storage.get_or("best_time", u32::MAX);
123 let mut text = format!("You found every beacon in {seconds} seconds.");
124 if seconds < best {
125 ctx.storage.set("best_time", seconds);
126 text.push_str(" That is a new record!");
127 } else {
128 text.push_str(&format!(" Your record is {best} seconds."));
129 }
130 ctx.ui.message("done", text);
131 }More examples
8fn main() -> audio_code::Result<()> {
9 let mut score = 0;
10
11 audio_code::run("Clicker", move |ctx| {
12 if ctx.once("intro") {
13 let best: u32 = ctx.storage.get_or("best", 0);
14 ctx.speak(format!("Press space as often as you can in ten seconds. Your best is {best}. Go!"));
15 ctx.time.after("end", 10.0);
16 }
17
18 if ctx.time.is_scheduled("end") && ctx.pressed(Key::Space) {
19 score += 1;
20 ctx.audio.play_tone(300.0 + score as f32 * 15.0, 0.05);
21 }
22
23 if ctx.time.fired("end") {
24 let best: u32 = ctx.storage.get_or("best", 0);
25 if score > best {
26 ctx.storage.set("best", score);
27 ctx.ui.message("result", format!("{score} presses. New record!"));
28 } else {
29 ctx.ui.message("result", format!("{score} presses. Your best is {best}."));
30 }
31 }
32
33 if ctx.ui.closed("result") || ctx.pressed(Key::Escape) {
34 ctx.quit();
35 }
36 })
37}92 fn on_ui(&mut self, event: &UiEvent, ctx: &mut Context) {
93 match event {
94 UiEvent::Activated { menu, item } if menu == "title" => match item.as_str() {
95 "play" => {
96 ctx.ui.close_all();
97 ctx.switch_scene(Game::default());
98 }
99 "voice" => match voice_menu(ctx) {
100 Some(menu) => ctx.ui.open(menu),
101 None => ctx.ui.message(
102 "no voices",
103 "Your screen reader speaks with its own voice. Change it in the screen reader's settings.",
104 ),
105 },
106 "help" => ctx.ui.message(
107 "help",
108 "Bells fall in one of seven lanes in front of you. Listen where a bell rings, \
109 step under it with left and right, and catch it before it lands. \
110 The bell rings faster and higher the closer it gets. You have three lives. \
111 Press S for your score, Escape for the menu.",
112 ),
113 "quit" => ctx.quit(),
114 _ => {}
115 },
116 UiEvent::Activated { menu, item } if menu == "voices" => {
117 if item == "back" {
118 ctx.ui.close();
119 return;
120 }
121 if item == "default" {
122 ctx.speech.reset_voice();
123 ctx.storage.remove("voice");
124 } else if let Some(voice) =
125 item.strip_prefix("voice").and_then(|i| i.parse::<usize>().ok()).and_then(|i| ctx.speech.voices().get(i).cloned())
126 && ctx.speech.set_voice(&voice.id)
127 {
128 ctx.storage.set("voice", &voice.id);
129 }
130 ctx.ui.close();
131 refresh_title(ctx, "voice");
132 let name = ctx.speech.voice().map_or("Default voice".to_string(), |v| v.name.clone());
133 ctx.speech.speak_queued(format!("{name}. This is how I sound now."));
134 }
135 UiEvent::Changed { menu, item, value } if menu == "title" && item == "rate" => {
136 ctx.speech.set_rate(value.as_f32() / 100.0);
137 ctx.storage.set("rate", value.as_f32());
138 }
139 _ => {}
140 }
141 }
142
143 fn update(&mut self, _ctx: &mut Context) {}
144}
145
146// ------------------------------------------------------------------------- game
147
148struct Game {
149 lane: i32,
150 bell_lane: i32,
151 /// Seconds a bell needs to land; shrinks with every catch.
152 fall: f32,
153 /// Seconds until the current bell lands. Negative while waiting for the next bell.
154 left: f32,
155 next_ring: f32,
156 score: u32,
157 lives: u32,
158 over: bool,
159}
160
161impl Default for Game {
162 fn default() -> Self {
163 Self { lane: 0, bell_lane: 0, fall: FIRST_FALL, left: 0.0, next_ring: 0.0, score: 0, lives: LIVES, over: false }
164 }
165}
166
167impl Game {
168 fn drop_bell(&mut self) {
169 self.bell_lane = random::int(-LANES as i64, LANES as i64) as i32;
170 self.left = self.fall;
171 self.next_ring = 0.0;
172 }
173
174 /// Where the bell is, seen from the player: lanes to the side, a little in front.
175 fn bell_position(&self) -> Vec3 {
176 vec3((self.bell_lane - self.lane) as f32 * LANE_WIDTH, 2.0, 0.0)
177 }
178
179 fn ring(&mut self, ctx: &mut Context) {
180 // 0.0 when the bell starts falling, 1.0 when it lands.
181 let progress = 1.0 - self.left / self.fall;
182 let position = self.bell_position();
183 ctx.audio.sound("bell").at(position).pitch(1.0 + progress * 0.6).play();
184 self.next_ring = 0.12 + 0.45 * (1.0 - progress);
185 }
186
187 fn land(&mut self, ctx: &mut Context) {
188 if self.lane == self.bell_lane {
189 self.score += 1;
190 self.fall = (self.fall * 0.93).max(FASTEST_FALL);
191 ctx.play("catch");
192 if self.score.is_multiple_of(5) {
193 ctx.speak(format!("{} bells!", self.score));
194 }
195 } else {
196 self.lives -= 1;
197 ctx.play("miss");
198 match self.lives {
199 0 => return self.game_over(ctx),
200 1 => ctx.speak("Missed! Last life."),
201 n => ctx.speak(format!("Missed! {n} lives left.")),
202 }
203 }
204 // A short break before the next bell.
205 self.left = -0.8;
206 }
207
208 fn game_over(&mut self, ctx: &mut Context) {
209 self.over = true;
210 let best: u32 = ctx.storage.get_or("best", 0);
211 let text = if self.score > best {
212 ctx.storage.set("best", self.score);
213 format!("Game over. {} bells, a new best score!", self.score)
214 } else {
215 format!("Game over. {} bells. Your best is {best}.", self.score)
216 };
217 ctx.ui.message("over", text);
218 }Sourcepub fn add(&mut self, key: &str, amount: i64) -> i64
pub fn add(&mut self, key: &str, amount: i64) -> i64
Adds amount to a number (missing counts as 0) and returns the new value.
Handy for statistics: ctx.storage.add("games_played", 1).
Sourcepub fn remove(&mut self, key: &str)
pub fn remove(&mut self, key: &str)
Removes a value.
Examples found in repository?
92 fn on_ui(&mut self, event: &UiEvent, ctx: &mut Context) {
93 match event {
94 UiEvent::Activated { menu, item } if menu == "title" => match item.as_str() {
95 "play" => {
96 ctx.ui.close_all();
97 ctx.switch_scene(Game::default());
98 }
99 "voice" => match voice_menu(ctx) {
100 Some(menu) => ctx.ui.open(menu),
101 None => ctx.ui.message(
102 "no voices",
103 "Your screen reader speaks with its own voice. Change it in the screen reader's settings.",
104 ),
105 },
106 "help" => ctx.ui.message(
107 "help",
108 "Bells fall in one of seven lanes in front of you. Listen where a bell rings, \
109 step under it with left and right, and catch it before it lands. \
110 The bell rings faster and higher the closer it gets. You have three lives. \
111 Press S for your score, Escape for the menu.",
112 ),
113 "quit" => ctx.quit(),
114 _ => {}
115 },
116 UiEvent::Activated { menu, item } if menu == "voices" => {
117 if item == "back" {
118 ctx.ui.close();
119 return;
120 }
121 if item == "default" {
122 ctx.speech.reset_voice();
123 ctx.storage.remove("voice");
124 } else if let Some(voice) =
125 item.strip_prefix("voice").and_then(|i| i.parse::<usize>().ok()).and_then(|i| ctx.speech.voices().get(i).cloned())
126 && ctx.speech.set_voice(&voice.id)
127 {
128 ctx.storage.set("voice", &voice.id);
129 }
130 ctx.ui.close();
131 refresh_title(ctx, "voice");
132 let name = ctx.speech.voice().map_or("Default voice".to_string(), |v| v.name.clone());
133 ctx.speech.speak_queued(format!("{name}. This is how I sound now."));
134 }
135 UiEvent::Changed { menu, item, value } if menu == "title" && item == "rate" => {
136 ctx.speech.set_rate(value.as_f32() / 100.0);
137 ctx.storage.set("rate", value.as_f32());
138 }
139 _ => {}
140 }
141 }Trait Implementations§
Auto Trait Implementations§
impl Freeze for Storage
impl RefUnwindSafe for Storage
impl Send for Storage
impl Sync for Storage
impl Unpin for Storage
impl UnsafeUnpin for Storage
impl UnwindSafe for Storage
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
Source§impl<T> Downcast for Twhere
T: Any,
impl<T> Downcast for Twhere
T: Any,
Source§fn into_any(self: Box<T>) -> Box<dyn Any>
fn into_any(self: Box<T>) -> Box<dyn Any>
Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>. Box<dyn Any> can
then be further downcast into Box<ConcreteType> where ConcreteType implements Trait.Source§fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
Rc<Trait> (where Trait: Downcast) to Rc<Any>. Rc<Any> can then be
further downcast into Rc<ConcreteType> where ConcreteType implements Trait.Source§fn as_any(&self) -> &(dyn Any + 'static)
fn as_any(&self) -> &(dyn Any + 'static)
&Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &Any’s vtable from &Trait’s.Source§fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
&mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &mut Any’s vtable from &mut Trait’s.