good_web_game/
filesystem.rs

1// large parts directly stolen from macroquad: https://github.com/not-fl3/macroquad/blob/854aa50302a00ce590d505e28c9ecc42ae24be58/src/file.rs
2
3use std::sync::{Arc, Mutex};
4use std::{collections::HashMap, io, path};
5
6use crate::GameError::ResourceLoadError;
7use crate::{conf::Conf, Context, GameError, GameResult};
8use std::panic::panic_any;
9
10#[derive(Debug, Clone)]
11pub struct File {
12    pub bytes: io::Cursor<Vec<u8>>,
13}
14
15impl io::Read for File {
16    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
17        self.bytes.read(buf)
18    }
19}
20
21/// A structure that contains the filesystem state and cache.
22#[derive(Debug)]
23pub struct Filesystem {
24    root: Option<path::PathBuf>,
25    files: HashMap<path::PathBuf, File>,
26}
27
28impl Filesystem {
29    #[allow(clippy::redundant_closure)]
30    pub(crate) fn new(conf: &Conf) -> Filesystem {
31        let mut files = HashMap::new();
32
33        if let Some(tar_file) = conf.cache {
34            let mut archive = tar::Archive::new(tar_file);
35
36            for file in archive.entries().unwrap_or_else(|e| panic_any(e)) {
37                use std::io::Read;
38
39                let mut file = file.unwrap_or_else(|e| panic_any(e));
40                let filename =
41                    std::path::PathBuf::from(file.path().unwrap_or_else(|e| panic_any(e)));
42                let mut buf = vec![];
43
44                file.read_to_end(&mut buf).unwrap_or_else(|e| panic_any(e));
45                if !buf.is_empty() {
46                    files.insert(
47                        filename,
48                        File {
49                            bytes: io::Cursor::new(buf),
50                        },
51                    );
52                }
53            }
54        }
55
56        let root = conf.physical_root_dir.clone();
57        Filesystem { root, files }
58    }
59
60    /// Opens the given `path` and returns the resulting `File`
61    /// in read-only mode.
62    pub fn open<P: AsRef<path::Path>>(&mut self, path: P) -> GameResult<File> {
63        let mut path = path::PathBuf::from(path.as_ref());
64
65        // workaround for ggez-style pathes: in ggez paths starts with "/", while in the cache
66        // dictionary they are presented without "/"
67        if let Ok(stripped) = path.strip_prefix("/") {
68            path = path::PathBuf::from(stripped);
69        }
70
71        // first check the cache
72        if self.files.contains_key(&path) {
73            Ok(self.files[&path].clone())
74        } else {
75            // the file is not inside the cache, so it has to be loaded (locally, or via http url)
76            let file = self.load_file(&path)?;
77            Ok(file)
78        }
79    }
80
81    #[cfg(not(target_os = "wasm32"))]
82    /// Load file from the path and block until its loaded
83    /// Will use filesystem on PC and Android and fail on WASM
84    fn load_file<P: AsRef<path::Path>>(&self, path: P) -> GameResult<File> {
85        fn load_file_inner(path: &str) -> GameResult<Vec<u8>> {
86            let contents = Arc::new(Mutex::new(None));
87
88            {
89                let contents = contents.clone();
90                let err_path = path.to_string();
91
92                miniquad::fs::load_file(path, move |bytes| {
93                    *contents.lock().unwrap() = Some(bytes.map_err(|kind| {
94                        GameError::ResourceLoadError(format!(
95                            "Couldn't load file {}: {}",
96                            err_path, kind
97                        ))
98                    }));
99                });
100            }
101
102            // wait until the file has been loaded
103            // as miniquad::fs::load_file internally uses non-asynchronous loading for everything
104            // except wasm, waiting should only ever occur on wasm (TODO: since this holds the main
105            // thread hostage no progress is ever made and this just blocks forever... perhaps this
106            // could be worked around by using "asyncify", but that would be both hard and also
107            // require an additional post processing step on the generated wasm file)
108            loop {
109                let mut contents_guard = contents.lock().unwrap();
110                if let Some(contents) = contents_guard.take() {
111                    return contents;
112                }
113                drop(contents_guard);
114                std::thread::yield_now();
115            }
116        }
117
118        #[cfg(target_os = "ios")]
119        let _ = std::env::set_current_dir(std::env::current_exe().unwrap().parent().unwrap());
120
121        let path = path
122            .as_ref()
123            .as_os_str()
124            .to_os_string()
125            .into_string()
126            .map_err(|os_string| {
127                ResourceLoadError(format!("utf-8-invalid path: {:?}", os_string))
128            })?;
129
130        #[cfg(not(target_os = "android"))]
131        let path = if let Some(ref root) = self.root {
132            format!(
133                "{}/{}",
134                root.as_os_str()
135                    .to_os_string()
136                    .into_string()
137                    .map_err(|os_string| ResourceLoadError(format!(
138                        "utf-8-invalid root: {:?}",
139                        os_string
140                    )))?,
141                path
142            )
143        } else {
144            path
145        };
146
147        let buf = load_file_inner(&path)?;
148        let bytes = io::Cursor::new(buf);
149        Ok(File { bytes })
150    }
151
152    #[cfg(target_os = "wasm32")]
153    /// Load file from the path and block until its loaded
154    /// Will use filesystem on PC and Android and fail on WASM
155    fn load_file<P: AsRef<path::Path>>(&self, path: P) -> GameResult<File> {
156        Err(GameError::ResourceLoadError(format!(
157            "Couldn't load file {}",
158            path.as_display()
159        )))
160    }
161}
162
163/// Opens the given path and returns the resulting `File`
164/// in read-only mode.
165///
166/// Checks the [embedded tar file](../conf/struct.Conf.html#method.high_dpi), if there is one, first and if the file cannot be found there
167/// continues to either load the file using the OS-filesystem, or just fail on WASM, as blocking loads
168/// are impossible there.
169pub fn open<P: AsRef<path::Path>>(ctx: &mut Context, path: P) -> GameResult<File> {
170    ctx.filesystem.open(path)
171}
172
173/// Loads a file from the path returning an `Option` that will be `Some` once it has been loaded (or loading it failed).
174/// Will use filesystem on PC and Android and a http request on WASM.
175///
176/// Note: Don't wait for the `Option` to become `Some` inside of a loop, as that would create an infinite loop
177/// on WASM, where progress on the GET request can only be made _between_ frames of your application.
178pub fn load_file_async<P: AsRef<path::Path>>(path: P) -> Arc<Mutex<Option<GameResult<File>>>> {
179    // TODO: Create an example showcasing the use of this.
180    let contents = Arc::new(Mutex::new(None));
181    let path = path
182        .as_ref()
183        .as_os_str()
184        .to_os_string()
185        .into_string()
186        .map_err(|os_string| ResourceLoadError(format!("utf-8-invalid path: {:?}", os_string)));
187
188    if let Ok(path) = path {
189        let contents = contents.clone();
190
191        miniquad::fs::load_file(&*(path.clone()), move |response| {
192            let result = match response {
193                Ok(bytes) => Ok(File {
194                    bytes: io::Cursor::new(bytes),
195                }),
196                Err(e) => Err(GameError::ResourceLoadError(format!(
197                    "Couldn't load file {}: {}",
198                    path, e
199                ))),
200            };
201            *contents.lock().unwrap() = Some(result);
202        });
203    }
204
205    contents
206}