Skip to main content

firefly_runtime/
state.rs

1use crate::battery::Battery;
2use crate::canvas::Canvas;
3use crate::color::Rgb16;
4use crate::config::FullID;
5use crate::error::RuntimeStats;
6use crate::error_scene::ErrorScene;
7use crate::frame_buffer::FrameBuffer;
8use crate::menu::{Menu, MenuItem};
9use crate::net::*;
10use crate::utils::{read_all, read_all_into};
11use crate::Error;
12use alloc::borrow::ToOwned;
13use alloc::boxed::Box;
14use core::cell::Cell;
15use core::fmt::Display;
16use core::str::FromStr;
17use embedded_graphics::pixelcolor::{Rgb888, RgbColor};
18use embedded_io::Write;
19use firefly_hal::*;
20use firefly_types::Encode;
21
22#[allow(private_interfaces)]
23pub enum NetHandler<'a> {
24    None,
25    Connector(Box<Connector<'a>>),
26    Connection(Box<Connection<'a>>),
27    FrameSyncer(Box<FrameSyncer<'a>>),
28}
29
30pub(crate) struct State<'a> {
31    /// Access to peripherals.
32    pub device: DeviceImpl<'a>,
33
34    pub rom_dir: DirImpl,
35
36    /// The app menu manager.
37    pub menu: Menu,
38
39    launcher: bool,
40
41    pub error: Option<ErrorScene>,
42
43    /// Audio manager.
44    pub audio: firefly_audio::Manager,
45
46    /// The id of the currently running app.
47    pub id: FullID,
48
49    /// The frame buffer.
50    pub frame: FrameBuffer,
51
52    /// An image in the guest memory that, if not None, used to graphics as draw target.
53    pub canvas: Option<Canvas>,
54
55    /// The current state of the randomization function.
56    pub seed: u32,
57
58    /// If true, the current seed is set by the app and must not be randomized
59    /// using true RNG.
60    pub lock_seed: bool,
61
62    /// Pointer to the app memory.
63    ///
64    /// Might be None if the app doesn't have guest memory defined.
65    pub memory: Option<wasmi::Memory>,
66
67    /// True if the app should be stopped.
68    pub exit: bool,
69
70    /// The next app to run.
71    pub next: Option<FullID>,
72
73    /// The last read touch pad and buttons input of the current device.
74    pub input: Option<InputState>,
75
76    /// The last called host function.
77    pub called: &'static str,
78
79    /// The device settings.
80    pub settings: firefly_types::Settings,
81
82    /// The battery status (State of Charge, aka SoC).
83    pub battery: Option<Battery>,
84
85    pub app_stats: Option<firefly_types::Stats>,
86    /// The number of update frames.
87    n_frames: u32,
88    pub stash: alloc::vec::Vec<u8>,
89    pub stash_dirty: bool,
90
91    pub net_handler: Cell<NetHandler<'a>>,
92    action: Action,
93}
94
95impl<'a> State<'a> {
96    /// Allocate new state on heap.
97    ///
98    /// We automatically box the state because it's relatively fat
99    /// (arund 1 Kb) for the embedded heap.
100    pub(crate) fn new(
101        id: FullID,
102        device: DeviceImpl<'a>,
103        rom_dir: DirImpl,
104        net_handler: NetHandler<'a>,
105        launcher: bool,
106    ) -> Box<Self> {
107        let seed = match &net_handler {
108            NetHandler::FrameSyncer(syncer) => syncer.shared_seed,
109            _ => 0,
110        };
111        let mut device = device;
112        let maybe_battery = Battery::new(&mut device);
113        let settings = load_settings(&mut device).unwrap_or_default();
114        Box::new(Self {
115            device,
116            rom_dir,
117            id,
118            frame: FrameBuffer::new(),
119            canvas: None,
120            menu: Menu::new(),
121            launcher,
122            error: None,
123            audio: firefly_audio::Manager::new(),
124            battery: maybe_battery.ok(),
125            seed,
126            lock_seed: false,
127            memory: None,
128            next: None,
129            exit: false,
130            input: None,
131            called: "",
132            net_handler: Cell::new(net_handler),
133            settings,
134            app_stats: None,
135            n_frames: 0,
136            stash: alloc::vec::Vec::new(),
137            stash_dirty: false,
138            action: Action::None,
139        })
140    }
141
142    /// Read app stats from FS.
143    pub(crate) fn load_app_stats(&mut self) -> Result<(), Error> {
144        let dir_path = &["data", self.id.author(), self.id.app()];
145        // TODO(@orsinium): figure out prettier error handling
146        //     without more overhead. `anyhow`?
147        let mut dir = match self.device.open_dir(dir_path) {
148            Ok(dir) => dir,
149            Err(err) => return Err(Error::OpenDir(dir_path.join("/"), err)),
150        };
151
152        let stream = match dir.open_file("stats") {
153            Ok(file) => file,
154            Err(err) => return Err(Error::OpenFile("stats", err)),
155        };
156        let raw = match read_all(stream) {
157            Ok(raw) => raw,
158            Err(err) => return Err(Error::ReadFile("stats", err.into())),
159        };
160        let stats = match firefly_types::Stats::decode(&raw) {
161            Ok(stats) => stats,
162            Err(err) => return Err(Error::DecodeStats(err)),
163        };
164        self.app_stats = Some(stats);
165        Ok(())
166    }
167
168    /// Read stash from FS.
169    pub(crate) fn load_stash(&mut self) -> Result<(), Error> {
170        let dir_path = &["data", self.id.author(), self.id.app()];
171        let mut dir = match self.device.open_dir(dir_path) {
172            Ok(dir) => dir,
173            Err(err) => return Err(Error::OpenDir(dir_path.join("/"), err)),
174        };
175
176        let stream = match dir.open_file("stash") {
177            Ok(file) => file,
178            Err(FSError::NotFound) => return Ok(()),
179            Err(err) => return Err(Error::OpenFile("stash", err)),
180        };
181        let res = read_all_into(stream, &mut self.stash);
182        if let Err(err) = res {
183            return Err(Error::ReadFile("stash", err.into()));
184        };
185        Ok(())
186    }
187
188    pub(crate) fn runtime_stats(&self) -> RuntimeStats {
189        RuntimeStats {
190            last_called: self.called,
191        }
192    }
193
194    /// Set ID of the next app to run and close the currently running one.
195    pub(crate) fn set_next(&mut self, app: Option<FullID>) {
196        match self.net_handler.get_mut() {
197            NetHandler::None | NetHandler::Connector(_) => {
198                self.next = app;
199                self.exit = true;
200            }
201            NetHandler::FrameSyncer(_) => {
202                let action = match app {
203                    Some(id) if id == self.id => Action::Restart,
204                    Some(_) => panic!("cannot launch another app in multiplayer"),
205                    None => Action::Exit,
206                };
207                self.action = action;
208            }
209            NetHandler::Connection(c) => {
210                let Some(app) = app else { return };
211                let res = c.set_app(&mut self.device, app);
212                if let Err(err) = res {
213                    self.device.log_error("netcode", err);
214                }
215            }
216        };
217    }
218
219    /// Dump stash (if any) on disk.
220    pub(crate) fn save_stash(&mut self) {
221        if !self.stash_dirty {
222            return;
223        }
224        let dir_path = &["data", self.id.author(), self.id.app()];
225        let mut dir = match self.device.open_dir(dir_path) {
226            Ok(dir) => dir,
227            Err(err) => {
228                self.device.log_error("stash", err);
229                return;
230            }
231        };
232
233        // If the stash is empty, remove the stash file instead of storing an empty file.
234        if self.stash.is_empty() {
235            let res = dir.remove_file("stash");
236            if let Err(err) = res {
237                self.device.log_error("stash", err);
238            }
239            return;
240        };
241
242        let mut stream = match dir.create_file("stash") {
243            Ok(stream) => stream,
244            Err(err) => {
245                self.device.log_error("stash", err);
246                return;
247            }
248        };
249        let res = stream.write_all(&self.stash[..]);
250        if let Err(err) = res {
251            let err = FSError::from(err);
252            self.device.log_error("stash", err);
253        }
254    }
255
256    /// Save into stats struct the stats from the current play.
257    ///
258    /// Called just before saving the stats to the disk.
259    pub(crate) fn update_app_stats(&mut self) {
260        let players = self.player_count().min(4);
261        let idx = players - 1;
262        let Some(stats) = self.app_stats.as_mut() else {
263            return;
264        };
265        stats.launches[idx] += 1;
266        let minutes = self.n_frames / 3600;
267        stats.minutes[idx] += minutes;
268        if minutes > stats.longest_play[idx] {
269            stats.longest_play[idx] = minutes;
270        }
271    }
272
273    /// Get the number of players currently online.
274    fn player_count(&mut self) -> usize {
275        match self.net_handler.get_mut() {
276            NetHandler::None => 1,
277            NetHandler::Connector(connector) => connector.peer_infos().len() + 1,
278            NetHandler::Connection(connection) => connection.peers.len(),
279            NetHandler::FrameSyncer(frame_syncer) => frame_syncer.peers.len(),
280        }
281    }
282
283    /// Dump app stats on disk.
284    pub(crate) fn save_app_stats(&mut self) {
285        let Some(stats) = &self.app_stats else {
286            return;
287        };
288        let res = match stats.encode_vec() {
289            Ok(res) => res,
290            Err(err) => {
291                self.device.log_error("stats", err);
292                return;
293            }
294        };
295        let dir_path = &["data", self.id.author(), self.id.app()];
296        let mut dir = match self.device.open_dir(dir_path) {
297            Ok(dir) => dir,
298            Err(err) => {
299                self.device.log_error("stats", err);
300                return;
301            }
302        };
303        let mut stream = match dir.create_file("stats") {
304            Ok(stream) => stream,
305            Err(err) => {
306                self.device.log_error("stats", err);
307                return;
308            }
309        };
310        let res = stream.write_all(&res);
311        if let Err(err) = res {
312            let err = FSError::from(err);
313            self.device.log_error("stats", err);
314        }
315    }
316
317    /// Update the state: read inputs, handle system commands.
318    pub(crate) fn update(&mut self) -> Option<u8> {
319        self.n_frames += 1;
320        if let Some(scene) = self.error.as_mut() {
321            let close = scene.update(&mut self.device);
322            if close {
323                self.error = None;
324            }
325        }
326
327        if self.error.is_none() {
328            self.input = self.device.read_input();
329        }
330        self.update_net();
331
332        // Get combined input for all peers.
333        //
334        // In offline mode, it's just the input.
335        // For multi-player game, it is the combined input of all player,
336        // unless in launcher (Connector or Connection).
337        // We use it to ensure that all players open the app menu simultaneously.
338        let input = match self.net_handler.get_mut() {
339            // single-player
340            NetHandler::None => self.input.clone(),
341            // shouldn't be reachable
342            NetHandler::Connector(_) => return None,
343            // in launcher
344            NetHandler::Connection(_) => self.input.clone(),
345            // in game
346            NetHandler::FrameSyncer(syncer) => {
347                // TODO: if menu is open, we need to adjust sync timeout
348                // for the frame syncer.
349                match &self.input {
350                    Some(input) => {
351                        // In frame syncer, use shared input for the menu button
352                        // (if one player presses it, press it for everyone)
353                        // and local input for all other buttons.
354                        let mut input = input.clone();
355                        if syncer.get_combined_input().menu() {
356                            input.buttons |= 0b10000;
357                        } else {
358                            input.buttons &= !0b10000;
359                        };
360                        Some(input)
361                    }
362                    None => {
363                        if syncer.get_combined_input().menu() {
364                            Some(InputState {
365                                pad: None,
366                                buttons: 0b10000,
367                            })
368                        } else {
369                            None
370                        }
371                    }
372                }
373            }
374        };
375
376        if !self.launcher {
377            let action = self.menu.handle_input(&input);
378            if let Some(action) = action {
379                match action {
380                    MenuItem::Custom(index, _) => return Some(*index),
381                    MenuItem::ScreenShot => self.take_screenshot(),
382                    MenuItem::Restart => self.set_next(Some(self.id.clone())),
383                    MenuItem::Quit => self.set_next(None),
384                };
385            };
386        }
387        None
388    }
389
390    fn update_net(&mut self) {
391        let handler = self.net_handler.replace(NetHandler::None);
392        let handler = match handler {
393            NetHandler::Connector(connector) => self.update_connector(connector),
394            NetHandler::None => NetHandler::None,
395            NetHandler::Connection(connection) => self.update_connection(connection),
396            NetHandler::FrameSyncer(syncer) => self.update_syncer(syncer),
397        };
398        self.net_handler.replace(handler);
399    }
400
401    fn update_connector<'b>(&mut self, mut connector: Box<Connector<'b>>) -> NetHandler<'b> {
402        let res = connector.update(&self.device);
403        if let Err(err) = res {
404            self.error = Some(ErrorScene::new(alloc::format!("{err}")));
405            self.device.log_error("netcode", err);
406            return NetHandler::Connector(connector);
407        }
408        let Some(mut conn_status) = connector.status else {
409            return NetHandler::Connector(connector);
410        };
411        // If the peers list contains only the current device itself,
412        // we can't start multiplayer: treat confirmation as cancellation.
413        if conn_status == ConnectStatus::Finished && connector.peer_infos().is_empty() {
414            conn_status = ConnectStatus::Cancelled;
415        }
416        match conn_status {
417            ConnectStatus::Stopped => {
418                let res = connector.pause();
419                if let Err(err) = res {
420                    self.device.log_error("netcode", err);
421                }
422                NetHandler::Connector(connector)
423            }
424            ConnectStatus::Cancelled => {
425                self.set_next(None);
426                let res = connector.cancel();
427                if let Err(err) = res {
428                    self.device.log_error("netcode", err);
429                }
430                NetHandler::None
431            }
432            ConnectStatus::Finished => {
433                if let Err(err) = connector.validate() {
434                    self.error = Some(ErrorScene::new(err.to_owned()))
435                }
436                self.set_next(None);
437                let connection = connector.finalize();
438                NetHandler::Connection(connection)
439            }
440        }
441    }
442
443    fn update_connection<'b>(&mut self, mut connection: Box<Connection<'b>>) -> NetHandler<'b> {
444        let status = connection.update(&mut self.device);
445        match status {
446            ConnectionStatus::Launching => {
447                if let Some(app_id) = &connection.app {
448                    self.set_next(Some(app_id.clone()));
449                    let syncer = connection.finalize(&mut self.device);
450                    return NetHandler::FrameSyncer(syncer);
451                }
452            }
453            ConnectionStatus::Timeout => {
454                let msg = "timed out waiting for other devices to launch the app";
455                self.device.log_error("netcode", msg);
456                self.set_next(None);
457                return NetHandler::None;
458            }
459            _ => (),
460        }
461        NetHandler::Connection(connection)
462    }
463
464    fn update_syncer<'b>(&mut self, mut syncer: Box<FrameSyncer<'b>>) -> NetHandler<'b> {
465        // * Don't sync seed if it is locked by the app (misc.set_seed was called).
466        // * Don't sync seed if misc.get_random was never called.
467        // * Don't sync seed too often to avoid poking true RNG too often.
468        let sync_rand = !self.lock_seed && self.seed != 0 && syncer.frame % 60 == 21;
469        let rand = if sync_rand { self.device.random() } else { 0 };
470
471        let input = self.input.clone().unwrap_or_default();
472        let frame_state = FrameState {
473            // No need to set frame number here,
474            // it will be set by FrameSyncer.advance.
475            frame: 0,
476            rand,
477            input: Input {
478                pad: input.pad.map(Into::into),
479                buttons: input.buttons,
480            },
481            action: self.action,
482        };
483
484        syncer.advance(&mut self.device, frame_state);
485        while !syncer.ready() {
486            let res = syncer.update(&self.device);
487            if let Err(err) = res {
488                self.device.log_error("netcode", err);
489                self.set_next(None);
490                return NetHandler::None;
491            }
492        }
493
494        let action = syncer.get_action();
495        match action {
496            Action::None => (),
497            Action::Restart => {
498                self.next = Some(self.id.clone());
499                self.exit = true;
500                self.menu.deactivate();
501            }
502            Action::Exit => {
503                self.exit = true;
504                self.menu.deactivate();
505                return NetHandler::Connection(syncer.into_connection());
506            }
507        }
508
509        if sync_rand {
510            let seed = syncer.get_seed();
511            if seed != 0 {
512                self.seed = seed;
513            }
514        }
515        NetHandler::FrameSyncer(syncer)
516    }
517
518    /// Save the current frame buffer into a PNG file.
519    pub fn take_screenshot(&mut self) {
520        let dir_path = &["data", self.id.author(), self.id.app(), "shots"];
521        let mut dir = match self.device.open_dir(dir_path) {
522            Ok(dir) => dir,
523            Err(err) => {
524                self.device.log_error("shot", err);
525                return;
526            }
527        };
528
529        let mut index = 1;
530        _ = dir.iter_dir(|_, _| index += 1);
531        let file_name = alloc::format!("{index:03}.ffs");
532
533        let mut file = match dir.create_file(&file_name) {
534            Ok(file) => file,
535            Err(err) => {
536                self.device.log_error("shot", err);
537                return;
538            }
539        };
540        let res = write_shot(&mut file, &self.frame.palette, &*self.frame.data);
541        if let Err(err) = res {
542            let err: firefly_hal::FSError = err.into();
543            self.device.log_error("shot", err);
544        }
545    }
546
547    pub fn connect(&mut self) {
548        if !matches!(self.net_handler.get_mut(), NetHandler::None) {
549            return;
550        }
551        let name = &self.settings.name;
552        let name = heapless::String::from_str(name).unwrap_or_default();
553        // TODO: validate the name
554        let s = &self.settings;
555        let flags = u8::from(s.rotate_screen)
556            | u8::from(s.reduce_flashing) << 1
557            | u8::from(s.contrast) << 2
558            | u8::from(s.easter_eggs) << 3;
559        let me = Intro {
560            name,
561            version: 1,
562            lang: s.lang,
563            country: s.country,
564            theme: s.theme,
565            flags,
566        };
567        let net = self.device.network();
568        self.net_handler
569            .set(NetHandler::Connector(Box::new(Connector::new(me, net))));
570        let id = FullID::from_str("sys", "connector").unwrap();
571        self.set_next(Some(id));
572    }
573
574    pub fn disconnect(&mut self) {
575        let net_handler = self.net_handler.replace(NetHandler::None);
576        if let NetHandler::Connection(conn) = net_handler {
577            let res = conn.disconnect();
578            if let Err(err) = res {
579                self.device.log_error("netcode", &err);
580                let msg = alloc::format!("{err}");
581                self.error = Some(ErrorScene::new(msg));
582            }
583        }
584    }
585
586    /// Log an error/warning occured in the currently executing host function.
587    pub(crate) fn log_error<D: Display>(&self, msg: D) {
588        self.device.log_error(self.called, msg);
589    }
590}
591
592fn load_settings(device: &mut DeviceImpl) -> Option<firefly_types::Settings> {
593    let mut dir = match device.open_dir(&["sys"]) {
594        Ok(dir) => dir,
595        Err(err) => {
596            device.log_error("settings", err);
597            return None;
598        }
599    };
600    let file = match dir.open_file("config") {
601        Ok(file) => file,
602        Err(err) => {
603            device.log_error("settings", err);
604            return None;
605        }
606    };
607    let raw = match read_all(file) {
608        Ok(raw) => raw,
609        Err(err) => {
610            device.log_error("settings", FSError::from(err));
611            return None;
612        }
613    };
614    let settings = match firefly_types::Settings::decode(&raw[..]) {
615        Ok(settings) => settings,
616        Err(err) => {
617            device.log_error("settings", err);
618            return None;
619        }
620    };
621    Some(settings)
622}
623
624/// Write the frame buffer as a screenshot file.
625pub(crate) fn write_shot<W, E>(mut w: W, palette: &[Rgb16; 16], frame: &[u8]) -> Result<(), E>
626where
627    W: embedded_io::Write<Error = E>,
628{
629    w.write_all(&[0x41])?;
630    // TODO(@orsinium): what is faster: to write each byte directly into the file
631    // or, as it is now, create an array first and then write it in one go?
632    let palette = encode_palette(palette);
633    w.write_all(&palette)?;
634    w.write_all(frame)?;
635    Ok(())
636}
637
638/// Serialize the palette as continious RGB bytes.
639fn encode_palette(palette: &[Rgb16; 16]) -> [u8; 16 * 3] {
640    let mut encoded: [u8; 16 * 3] = [0; 16 * 3];
641    for (i, color) in palette.iter().enumerate() {
642        let color: Rgb888 = (*color).into();
643        let i = i * 3;
644        encoded[i] = color.r();
645        encoded[i + 1] = color.g();
646        encoded[i + 2] = color.b();
647    }
648    encoded
649}