pyxel/
old_resource_data.rs

1use std::fmt;
2use std::fs::File;
3use std::io::Read;
4use std::path::Path;
5
6use zip::ZipArchive;
7
8use crate::channel::{Note, Volume};
9use crate::image::{Color, Image, Rgb24};
10use crate::music::Music;
11use crate::oscillator::{Effect, ToneIndex};
12use crate::pyxel::Pyxel;
13use crate::settings::{
14    INITIAL_SOUND_SPEED, NUM_CHANNELS, NUM_IMAGES, NUM_MUSICS, NUM_SOUNDS, NUM_TILEMAPS,
15    PALETTE_FILE_EXTENSION, TILEMAP_SIZE, VERSION,
16};
17use crate::sound::Sound;
18use crate::tilemap::{ImageSource, ImageTileCoord, Tilemap};
19use crate::utils::{parse_hex_string, simplify_string};
20
21pub const RESOURCE_ARCHIVE_DIRNAME: &str = "pyxel_resource/";
22
23trait ResourceItem {
24    fn resource_name(item_index: u32) -> String;
25    fn clear(&mut self);
26    fn deserialize(&mut self, version: u32, input: &str);
27}
28
29impl ResourceItem for Image {
30    fn resource_name(item_index: u32) -> String {
31        RESOURCE_ARCHIVE_DIRNAME.to_string() + "image" + &item_index.to_string()
32    }
33
34    fn clear(&mut self) {
35        self.cls(0);
36    }
37
38    fn deserialize(&mut self, _version: u32, input: &str) {
39        for (i, line) in input.lines().enumerate() {
40            string_loop!(j, color, line, 1, {
41                self.canvas
42                    .write_data(j, i, parse_hex_string(&color).unwrap() as Color);
43            });
44        }
45    }
46}
47
48impl fmt::Display for ImageSource {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        match self {
51            ImageSource::Index(index) => write!(f, "{index}"),
52            ImageSource::Image(_) => write!(f, "0"),
53        }
54    }
55}
56
57impl ResourceItem for Tilemap {
58    fn resource_name(item_index: u32) -> String {
59        RESOURCE_ARCHIVE_DIRNAME.to_string() + "tilemap" + &item_index.to_string()
60    }
61
62    fn clear(&mut self) {
63        self.cls((0, 0));
64    }
65
66    fn deserialize(&mut self, version: u32, input: &str) {
67        for (y, line) in input.lines().enumerate() {
68            if y < TILEMAP_SIZE as usize {
69                if version < 10500 {
70                    string_loop!(x, tile, line, 3, {
71                        let tile = parse_hex_string(&tile).unwrap();
72                        self.canvas.write_data(
73                            x,
74                            y,
75                            ((tile % 32) as ImageTileCoord, (tile / 32) as ImageTileCoord),
76                        );
77                    });
78                } else {
79                    string_loop!(x, tile, line, 4, {
80                        let tile_x = parse_hex_string(&tile[0..2]).unwrap();
81                        let tile_y = parse_hex_string(&tile[2..4]).unwrap();
82                        self.canvas.write_data(
83                            x,
84                            y,
85                            (tile_x as ImageTileCoord, tile_y as ImageTileCoord),
86                        );
87                    });
88                }
89            } else {
90                self.imgsrc = ImageSource::Index(line.parse::<usize>().unwrap() as u32);
91            }
92        }
93    }
94}
95
96impl ResourceItem for Sound {
97    fn resource_name(item_index: u32) -> String {
98        RESOURCE_ARCHIVE_DIRNAME.to_string() + "sound" + &format!("{item_index:02}")
99    }
100
101    fn clear(&mut self) {
102        self.notes.clear();
103        self.tones.clear();
104        self.volumes.clear();
105        self.effects.clear();
106        self.speed = INITIAL_SOUND_SPEED;
107    }
108
109    fn deserialize(&mut self, _version: u32, input: &str) {
110        self.clear();
111
112        for (i, line) in input.lines().enumerate() {
113            if line == "none" {
114                continue;
115            }
116
117            if i == 0 {
118                string_loop!(j, value, line, 2, {
119                    self.notes
120                        .push(parse_hex_string(&value).unwrap() as i8 as Note);
121                });
122            } else if i == 1 {
123                string_loop!(j, value, line, 1, {
124                    self.tones
125                        .push(parse_hex_string(&value).unwrap() as ToneIndex);
126                });
127            } else if i == 2 {
128                string_loop!(j, value, line, 1, {
129                    self.volumes
130                        .push(parse_hex_string(&value).unwrap() as Volume);
131                });
132            } else if i == 3 {
133                string_loop!(j, value, line, 1, {
134                    self.effects
135                        .push(parse_hex_string(&value).unwrap() as Effect);
136                });
137            } else if i == 4 {
138                self.speed = line.parse().unwrap();
139            }
140        }
141    }
142}
143
144impl ResourceItem for Music {
145    fn resource_name(item_index: u32) -> String {
146        RESOURCE_ARCHIVE_DIRNAME.to_string() + "music" + &item_index.to_string()
147    }
148
149    fn clear(&mut self) {
150        self.seqs = (0..NUM_CHANNELS)
151            .map(|_| new_shared_type!(Vec::new()))
152            .collect();
153    }
154
155    fn deserialize(&mut self, _version: u32, input: &str) {
156        self.clear();
157
158        for (i, line) in input.lines().enumerate() {
159            if line == "none" {
160                continue;
161            }
162            string_loop!(j, value, line, 2, {
163                self.seqs[i].lock().push(parse_hex_string(&value).unwrap());
164            });
165        }
166    }
167}
168
169impl Pyxel {
170    pub fn load_old_resource(
171        &mut self,
172        archive: &mut ZipArchive<File>,
173        filename: &str,
174        include_images: bool,
175        include_tilemaps: bool,
176        include_sounds: bool,
177        include_musics: bool,
178    ) {
179        let version_name = RESOURCE_ARCHIVE_DIRNAME.to_string() + "version";
180        let contents = {
181            let mut file = archive.by_name(&version_name).unwrap();
182            let mut contents = String::new();
183            file.read_to_string(&mut contents).unwrap();
184            contents
185        };
186        let version = parse_version_string(&contents).unwrap();
187        assert!(
188            version <= parse_version_string(VERSION).unwrap(),
189            "Unsupported resource file version '{contents}'"
190        );
191
192        macro_rules! deserialize {
193            ($type: ty, $list: ident, $count: expr) => {
194                for i in 0..$count {
195                    if let Ok(mut file) = archive.by_name(&<$type>::resource_name(i)) {
196                        let mut input = String::new();
197                        file.read_to_string(&mut input).unwrap();
198                        self.$list.lock()[i as usize]
199                            .lock()
200                            .deserialize(version, &input);
201                    } else {
202                        self.$list.lock()[i as usize].lock().clear();
203                    }
204                }
205            };
206        }
207
208        if include_images {
209            deserialize!(Image, images, NUM_IMAGES);
210        }
211        if include_tilemaps {
212            deserialize!(Tilemap, tilemaps, NUM_TILEMAPS);
213        }
214        if include_sounds {
215            deserialize!(Sound, sounds, NUM_SOUNDS);
216        }
217        if include_musics {
218            deserialize!(Music, musics, NUM_MUSICS);
219        }
220
221        // Try to load Pyxel palette file
222        let filename = filename
223            .rfind('.')
224            .map_or(filename, |i| &filename[..i])
225            .to_string()
226            + PALETTE_FILE_EXTENSION;
227
228        if let Ok(mut file) = File::open(Path::new(&filename)) {
229            let mut contents = String::new();
230            file.read_to_string(&mut contents).unwrap();
231
232            let colors: Vec<Rgb24> = contents
233                .replace("\r\n", "\n")
234                .replace('\r', "\n")
235                .split('\n')
236                .filter(|s| !s.is_empty())
237                .map(|s| u32::from_str_radix(s.trim(), 16).unwrap() as Rgb24)
238                .collect();
239
240            self.colors.lock().clear();
241            self.colors.lock().extend(colors.iter());
242        }
243    }
244}
245
246fn parse_version_string(string: &str) -> Result<u32, &str> {
247    let mut version = 0;
248
249    for (i, number) in simplify_string(string).split('.').enumerate() {
250        let digit = number.len();
251        let number = if i > 0 && digit == 1 {
252            "0".to_string() + number
253        } else if i == 0 || digit == 2 {
254            number.to_string()
255        } else {
256            return Err("invalid version string");
257        };
258
259        if let Ok(number) = number.parse::<u32>() {
260            version = version * 100 + number;
261        } else {
262            return Err("invalid version string");
263        }
264    }
265
266    Ok(version)
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272
273    #[test]
274    fn test_parse_version_string() {
275        assert_eq!(parse_version_string("1.2.3"), Ok(10203));
276        assert_eq!(parse_version_string("12.34.5"), Ok(123405));
277        assert_eq!(parse_version_string("12.3.04"), Ok(120304));
278        assert_eq!(
279            parse_version_string("12.345.0"),
280            Err("invalid version string")
281        );
282        assert_eq!(
283            parse_version_string("12.0.345"),
284            Err("invalid version string")
285        );
286        assert_eq!(parse_version_string(" "), Err("invalid version string"));
287    }
288}