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 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}