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. Lazy loaded on demand.
80    ///
81    /// None if not cached.
82    settings: Option<firefly_types::Settings>,
83
84    /// The battery status (State of Charge, aka SoC).
85    pub battery: Option<Battery>,
86
87    pub app_stats: Option<firefly_types::Stats>,
88    pub app_stats_dirty: bool,
89    pub stash: alloc::vec::Vec<u8>,
90    pub stash_dirty: bool,
91
92    pub net_handler: Cell<NetHandler<'a>>,
93    action: Action,
94}
95
96impl<'a> State<'a> {
97    /// Allocate new state on heap.
98    ///
99    /// We automatically box the state because it's relatively fat
100    /// (arund 1 Kb) for the embedded heap.
101    pub(crate) fn new(
102        id: FullID,
103        device: DeviceImpl<'a>,
104        rom_dir: DirImpl,
105        net_handler: NetHandler<'a>,
106        launcher: bool,
107    ) -> Box<Self> {
108        let seed = match &net_handler {
109            NetHandler::FrameSyncer(syncer) => syncer.shared_seed,
110            _ => 0,
111        };
112        let mut device = device;
113        let maybe_battery = Battery::new(&mut device);
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: None,
134            app_stats: None,
135            app_stats_dirty: false,
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    pub(crate) fn get_settings(&mut self) -> &mut firefly_types::Settings {
220        use crate::alloc::string::ToString;
221        if self.settings.is_none() {
222            let settings = self.load_settings();
223            self.settings = match settings {
224                Some(settings) => Some(settings),
225                None => Some(firefly_types::Settings {
226                    xp: 0,
227                    badges: 0,
228                    lang: [b'e', b'n'],
229                    name: "anonymous".to_string(),
230                    timezone: "Europe/Amsterdam".to_string(),
231                }),
232            }
233        }
234        self.settings.as_mut().unwrap()
235    }
236
237    fn load_settings(&mut self) -> Option<firefly_types::Settings> {
238        let mut dir = match self.device.open_dir(&["sys"]) {
239            Ok(dir) => dir,
240            Err(err) => {
241                self.device.log_error("settings", err);
242                return None;
243            }
244        };
245        let file = match dir.open_file("config") {
246            Ok(file) => file,
247            Err(err) => {
248                self.device.log_error("settings", err);
249                return None;
250            }
251        };
252        let raw = match read_all(file) {
253            Ok(raw) => raw,
254            Err(err) => {
255                self.device.log_error("settings", FSError::from(err));
256                return None;
257            }
258        };
259        let settings = match firefly_types::Settings::decode(&raw[..]) {
260            Ok(settings) => settings,
261            Err(err) => {
262                self.device.log_error("settings", err);
263                return None;
264            }
265        };
266        Some(settings)
267    }
268
269    /// Dump stash (if any) on disk.
270    pub(crate) fn save_stash(&mut self) {
271        if !self.stash_dirty {
272            return;
273        }
274        let dir_path = &["data", self.id.author(), self.id.app()];
275        let mut dir = match self.device.open_dir(dir_path) {
276            Ok(dir) => dir,
277            Err(err) => {
278                self.device.log_error("stash", err);
279                return;
280            }
281        };
282
283        // If the stash is empty, remove the stash file instead of storing an empty file.
284        if self.stash.is_empty() {
285            let res = dir.remove_file("stash");
286            if let Err(err) = res {
287                self.device.log_error("stash", err);
288            }
289            return;
290        };
291
292        let mut stream = match dir.create_file("stash") {
293            Ok(stream) => stream,
294            Err(err) => {
295                self.device.log_error("stash", err);
296                return;
297            }
298        };
299        let res = stream.write_all(&self.stash[..]);
300        if let Err(err) = res {
301            let err = FSError::from(err);
302            self.device.log_error("stash", err);
303        }
304    }
305
306    /// Save into stats the stats from the current play.
307    ///
308    /// Called jut before saving the stats to the disk.
309    pub(crate) fn update_app_stats(&mut self) {
310        let players = self.player_count();
311        let idx = players - 1;
312        let Some(stats) = self.app_stats.as_mut() else {
313            return;
314        };
315        self.app_stats_dirty = true;
316        stats.launches[idx] += 1;
317    }
318
319    /// Get the number of players currently online.
320    fn player_count(&mut self) -> usize {
321        match self.net_handler.get_mut() {
322            NetHandler::None => 1,
323            NetHandler::Connector(connector) => connector.peer_infos().len() + 1,
324            NetHandler::Connection(connection) => connection.peers.len(),
325            NetHandler::FrameSyncer(frame_syncer) => frame_syncer.peers.len(),
326        }
327    }
328
329    /// Dump app stats (if changed) on disk.
330    pub(crate) fn save_app_stats(&mut self) {
331        let Some(stats) = &self.app_stats else {
332            return;
333        };
334        if !self.app_stats_dirty {
335            return;
336        }
337        let res = match stats.encode_vec() {
338            Ok(res) => res,
339            Err(err) => {
340                self.device.log_error("stats", err);
341                return;
342            }
343        };
344        let dir_path = &["data", self.id.author(), self.id.app()];
345        let mut dir = match self.device.open_dir(dir_path) {
346            Ok(dir) => dir,
347            Err(err) => {
348                self.device.log_error("stats", err);
349                return;
350            }
351        };
352        let mut stream = match dir.create_file("stats") {
353            Ok(stream) => stream,
354            Err(err) => {
355                self.device.log_error("stats", err);
356                return;
357            }
358        };
359        let res = stream.write_all(&res);
360        if let Err(err) = res {
361            let err = FSError::from(err);
362            self.device.log_error("stats", err);
363        }
364    }
365
366    /// Update the state: read inputs, handle system commands.
367    pub(crate) fn update(&mut self) -> Option<u8> {
368        if let Some(scene) = self.error.as_mut() {
369            let close = scene.update(&mut self.device);
370            if close {
371                self.error = None;
372            }
373        }
374
375        if self.error.is_none() {
376            self.input = self.device.read_input();
377        }
378        self.update_net();
379
380        // Get combined input for all peers.
381        //
382        // In offline mode, it's just the input.
383        // For multi-player game, it is the combined input of all player,
384        // unless in launcher (Connector or Connection).
385        // We use it to ensure that all players open the app menu simultaneously.
386        let input = match self.net_handler.get_mut() {
387            // single-player
388            NetHandler::None => self.input.clone(),
389            // shouldn't be reachable
390            NetHandler::Connector(_) => return None,
391            // in launcher
392            NetHandler::Connection(_) => self.input.clone(),
393            // in game
394            NetHandler::FrameSyncer(syncer) => {
395                // TODO: if menu is open, we need to adjust sync timeout
396                // for the frame syncer.
397                match &self.input {
398                    Some(input) => {
399                        // In frame syncer, use shared input for the menu button
400                        // (if one player presses it, press it for everyone)
401                        // and local input for all other buttons.
402                        let mut input = input.clone();
403                        if syncer.get_combined_input().menu() {
404                            input.buttons |= 0b10000;
405                        } else {
406                            input.buttons &= !0b10000;
407                        };
408                        Some(input)
409                    }
410                    None => {
411                        if syncer.get_combined_input().menu() {
412                            Some(InputState {
413                                pad: None,
414                                buttons: 0b10000,
415                            })
416                        } else {
417                            None
418                        }
419                    }
420                }
421            }
422        };
423
424        if !self.launcher {
425            let action = self.menu.handle_input(&input);
426            if let Some(action) = action {
427                match action {
428                    MenuItem::Custom(index, _) => return Some(*index),
429                    MenuItem::ScreenShot => self.take_screenshot(),
430                    MenuItem::Restart => self.set_next(Some(self.id.clone())),
431                    MenuItem::Quit => self.set_next(None),
432                };
433            };
434        }
435        None
436    }
437
438    fn update_net(&mut self) {
439        let handler = self.net_handler.replace(NetHandler::None);
440        let handler = match handler {
441            NetHandler::Connector(connector) => self.update_connector(connector),
442            NetHandler::None => NetHandler::None,
443            NetHandler::Connection(connection) => self.update_connection(connection),
444            NetHandler::FrameSyncer(syncer) => self.update_syncer(syncer),
445        };
446        self.net_handler.replace(handler);
447    }
448
449    fn update_connector<'b>(&mut self, mut connector: Box<Connector<'b>>) -> NetHandler<'b> {
450        let res = connector.update(&self.device);
451        if let Err(err) = res {
452            self.error = Some(ErrorScene::new(alloc::format!("{err}")));
453            self.device.log_error("netcode", err);
454            return NetHandler::Connector(connector);
455        }
456        let Some(mut conn_status) = connector.status else {
457            return NetHandler::Connector(connector);
458        };
459        // If the peers list contains only the current device itself,
460        // we can't start multiplayer: treat confirmation as cancellation.
461        if conn_status == ConnectStatus::Finished && connector.peer_infos().is_empty() {
462            conn_status = ConnectStatus::Cancelled;
463        }
464        match conn_status {
465            ConnectStatus::Stopped => {
466                let res = connector.pause();
467                if let Err(err) = res {
468                    self.device.log_error("netcode", err);
469                }
470                NetHandler::Connector(connector)
471            }
472            ConnectStatus::Cancelled => {
473                self.set_next(None);
474                let res = connector.cancel();
475                if let Err(err) = res {
476                    self.device.log_error("netcode", err);
477                }
478                NetHandler::None
479            }
480            ConnectStatus::Finished => {
481                if let Err(err) = connector.validate() {
482                    self.error = Some(ErrorScene::new(err.to_owned()))
483                }
484                self.set_next(None);
485                let connection = connector.finalize();
486                NetHandler::Connection(connection)
487            }
488        }
489    }
490
491    fn update_connection<'b>(&mut self, mut connection: Box<Connection<'b>>) -> NetHandler<'b> {
492        let status = connection.update(&mut self.device);
493        match status {
494            ConnectionStatus::Launching => {
495                if let Some(app_id) = &connection.app {
496                    self.set_next(Some(app_id.clone()));
497                    let syncer = connection.finalize(&mut self.device);
498                    return NetHandler::FrameSyncer(syncer);
499                }
500            }
501            ConnectionStatus::Timeout => {
502                let msg = "timed out waiting for other devices to launch the app";
503                self.device.log_error("netcode", msg);
504                self.set_next(None);
505                return NetHandler::None;
506            }
507            _ => (),
508        }
509        NetHandler::Connection(connection)
510    }
511
512    fn update_syncer<'b>(&mut self, mut syncer: Box<FrameSyncer<'b>>) -> NetHandler<'b> {
513        // * Don't sync seed if it is locked by the app (misc.set_seed was called).
514        // * Don't sync seed if misc.get_random was never called.
515        // * Don't sync seed too often to avoid poking true RNG too often.
516        let sync_rand = !self.lock_seed && self.seed != 0 && syncer.frame % 60 == 21;
517        let rand = if sync_rand { self.device.random() } else { 0 };
518
519        let input = self.input.clone().unwrap_or_default();
520        let frame_state = FrameState {
521            // No need to set frame number here,
522            // it will be set by FrameSyncer.advance.
523            frame: 0,
524            rand,
525            input: Input {
526                pad: input.pad.map(Into::into),
527                buttons: input.buttons,
528            },
529            action: self.action,
530        };
531
532        syncer.advance(&mut self.device, frame_state);
533        while !syncer.ready() {
534            let res = syncer.update(&self.device);
535            if let Err(err) = res {
536                self.device.log_error("netcode", err);
537                self.set_next(None);
538                return NetHandler::None;
539            }
540        }
541
542        let action = syncer.get_action();
543        match action {
544            Action::None => (),
545            Action::Restart => {
546                self.next = Some(self.id.clone());
547                self.exit = true;
548                self.menu.deactivate();
549            }
550            Action::Exit => {
551                self.exit = true;
552                self.menu.deactivate();
553                return NetHandler::Connection(syncer.into_connection());
554            }
555        }
556
557        if sync_rand {
558            let seed = syncer.get_seed();
559            if seed != 0 {
560                self.seed = seed;
561            }
562        }
563        NetHandler::FrameSyncer(syncer)
564    }
565
566    /// Save the current frame buffer into a PNG file.
567    pub fn take_screenshot(&mut self) {
568        let dir_path = &["data", self.id.author(), self.id.app(), "shots"];
569        let mut dir = match self.device.open_dir(dir_path) {
570            Ok(dir) => dir,
571            Err(err) => {
572                self.device.log_error("shot", err);
573                return;
574            }
575        };
576
577        let mut index = 1;
578        _ = dir.iter_dir(|_, _| index += 1);
579        let file_name = alloc::format!("{index:03}.ffs");
580
581        let mut file = match dir.create_file(&file_name) {
582            Ok(file) => file,
583            Err(err) => {
584                self.device.log_error("shot", err);
585                return;
586            }
587        };
588        let res = write_shot(&mut file, &self.frame.palette, &*self.frame.data);
589        if let Err(err) = res {
590            let err: firefly_hal::FSError = err.into();
591            self.device.log_error("shot", err);
592        }
593    }
594
595    pub fn connect(&mut self) {
596        if !matches!(self.net_handler.get_mut(), NetHandler::None) {
597            return;
598        }
599        let name = &self.get_settings().name;
600        let name = heapless::String::from_str(name).unwrap_or_default();
601        // TODO: validate the name
602        let me = MyInfo { name, version: 1 };
603        let net = self.device.network();
604        self.net_handler
605            .set(NetHandler::Connector(Box::new(Connector::new(me, net))));
606        let id = FullID::from_str("sys", "connector").unwrap();
607        self.set_next(Some(id));
608    }
609
610    pub fn disconnect(&mut self) {
611        let net_handler = self.net_handler.replace(NetHandler::None);
612        if let NetHandler::Connection(conn) = net_handler {
613            let res = conn.disconnect();
614            if let Err(err) = res {
615                self.device.log_error("netcode", err);
616            }
617        }
618    }
619
620    /// Log an error/warning occured in the currently executing host function.
621    pub(crate) fn log_error<D: Display>(&self, msg: D) {
622        self.device.log_error(self.called, msg);
623    }
624}
625
626/// Write the frame buffer as a screenshot file.
627pub(crate) fn write_shot<W, E>(mut w: W, palette: &[Rgb16; 16], frame: &[u8]) -> Result<(), E>
628where
629    W: embedded_io::Write<Error = E>,
630{
631    w.write_all(&[0x41])?;
632    // TODO(@orsinium): what is faster: to write each byte directly into the file
633    // or, as it is now, create an array first and then write it in one go?
634    let palette = encode_palette(palette);
635    w.write_all(&palette)?;
636    w.write_all(frame)?;
637    Ok(())
638}
639
640/// Serialize the palette as continious RGB bytes.
641fn encode_palette(palette: &[Rgb16; 16]) -> [u8; 16 * 3] {
642    let mut encoded: [u8; 16 * 3] = [0; 16 * 3];
643    for (i, color) in palette.iter().enumerate() {
644        let color: Rgb888 = (*color).into();
645        let i = i * 3;
646        encoded[i] = color.r();
647        encoded[i + 1] = color.g();
648        encoded[i + 2] = color.b();
649    }
650    encoded
651}