sb3_decoder/structs/
target.rs

1//! The target module containing the [`Target`] enum.
2//!
3//! It also contains the [`Sprite`], [`Broadcast`] and [`Stage`] structs.
4
5use std::collections::HashMap;
6
7use indexmap::IndexMap;
8use zip::ZipArchive;
9
10use crate::{
11    decoder::{RawSprite, RawStage, RawTarget},
12    error::DecodeError,
13    structs::{decode_scripts, Costume, List, Script, Sound, Variable},
14};
15
16/// The [`Target`] enum represents either a sprite or the stage in a Scratch 3.0 project.
17#[derive(Debug, Clone)]
18pub enum Target {
19    /// A sprite target.
20    Sprite(Sprite),
21
22    /// The stage target.
23    Stage(Stage),
24}
25
26impl Target {
27    pub fn new(
28        raw: RawTarget,
29        zip: &mut ZipArchive<std::io::Cursor<Vec<u8>>>,
30    ) -> Result<Self, DecodeError> {
31        Ok(match raw {
32            RawTarget::Sprite(raw_sprite) => Target::Sprite(Sprite::new(raw_sprite, zip)?),
33            RawTarget::Stage(raw_stage) => Target::Stage(Stage::new(raw_stage, zip)?),
34        })
35    }
36}
37
38/// The [`Sprite`] struct represents a sprite in a Scratch 3.0 project.
39#[derive(Debug, Clone)]
40pub struct Sprite {
41    /// The name of the sprite.
42    pub name: String,
43
44    /// Contains all the variables of the sprite.
45    pub variables: HashMap<String, Variable>,
46
47    /// Contains all the lists of the sprite.
48    pub lists: HashMap<String, List>,
49
50    /// Contains all the scripts of the sprite.
51    pub scripts: Vec<Script>,
52
53    /// The index of the current costume.
54    pub current_costume: usize,
55
56    /// Contains all the costumes of the sprite.
57    costumes: IndexMap<String, Costume>,
58
59    /// Contains all the sounds of the sprite.
60    pub sounds: HashMap<String, Sound>,
61
62    /// The sprite's volume (0.0 = 0%, 1.0 = 100%)
63    pub volume: f32,
64
65    /// The layer of the sprite.
66    pub layer_order: isize,
67
68    /// If the sprite is visible.
69    pub visible: bool,
70
71    /// The position of the sprite.
72    pub position: (i32, i32),
73
74    /// The size of the sprite (0.0 = 0%, 1.0 = 100%)
75    pub size: f32,
76
77    /// The direction of the sprite in degrees.
78    pub direction: i32,
79
80    /// If the sprite is draggable.
81    pub draggable: bool,
82
83    /// The rotation style of the sprite.
84    pub rotation_style: RotationStyle,
85}
86
87impl Sprite {
88    /// Returns a new sprite from a [`RawSprite`] and the zip archive.
89    pub fn new(
90        raw: RawSprite,
91        zip: &mut ZipArchive<std::io::Cursor<Vec<u8>>>,
92    ) -> Result<Self, DecodeError> {
93        let variables = raw
94            .variables
95            .into_iter()
96            .map(|(_, raw_var)| (raw_var.name.clone(), Variable(raw_var.value.into())))
97            .collect();
98        #[cfg(feature = "costume_png")]
99        let costumes = raw
100            .costumes
101            .into_iter()
102            .map(|raw| {
103                let mut file = zip.by_name(&raw.md5ext)
104                    .map_err(|_| DecodeError::NotFound(raw.md5ext.clone()))?;
105                let mut buffer = Vec::new();
106                std::io::copy(&mut file, &mut buffer)?;
107                match raw.data_format.as_str() {
108                    "svg" => {
109                        let svg_data = std::str::from_utf8(&buffer).map_err(|_| {
110                            DecodeError::InvalidData("SVG data is not valid UTF-8".to_string())
111                        })?;
112                        let svg_tree =
113                            resvg::usvg::Tree::from_str(svg_data, &resvg::usvg::Options::default())
114                                .map_err(|e| {
115                                    DecodeError::InvalidData(format!("Failed to parse SVG: {}", e))
116                                })?;
117                        let image = crate::utils::rasterize_svg_to_png(
118                            &svg_tree,
119                            svg_tree.size().width() as u32,
120                            svg_tree.size().height() as u32,
121                        )
122                        .map_err(|e| {
123                            DecodeError::InvalidData(format!("Failed to rasterize SVG: {}", e))
124                        })?;
125                        Ok((raw.name, Costume(image)))
126                    }
127                    _ => {
128                        let image = image::load_from_memory(&buffer).map_err(|e| {
129                            DecodeError::InvalidData(format!("Failed to load image: {}", e))
130                        })?;
131                        Ok((raw.name, Costume(image)))
132                    }
133                }
134            })
135            .collect::<Result<IndexMap<String, Costume>, DecodeError>>()?;
136        #[cfg(feature = "costume_svg")]
137        let costumes = raw
138            .costumes
139            .into_iter()
140            .map(|raw| {
141                let mut file = zip.by_name(&raw.md5ext)
142                    .map_err(|_| DecodeError::NotFound(raw.md5ext.clone()))?;
143                let mut buffer = Vec::new();
144                std::io::copy(&mut file, &mut buffer)?;
145                match raw.data_format.as_str() {
146                    "svg" => {
147                        let svg_data = std::str::from_utf8(&buffer).map_err(|_| {
148                            DecodeError::InvalidData("SVG data is not valid UTF-8".to_string())
149                        })?;
150                        let svg_tree = usvg::Tree::from_str(svg_data, &usvg::Options::default())
151                            .map_err(|e| {
152                                DecodeError::InvalidData(format!("Failed to parse SVG: {}", e))
153                            })?;
154                        Ok((raw.name, Costume(svg_tree)))
155                    }
156                    _ => Err(DecodeError::InvalidData(format!(
157                        "Unsupported data format: {}",
158                        raw.data_format
159                    ))),
160                }
161            })
162            .collect::<Result<IndexMap<String, Costume>, DecodeError>>()?;
163        let sounds = raw
164            .sounds
165            .into_iter()
166            .map(|raw| {
167                let mut file = zip.by_name(&raw.md5ext)
168                    .map_err(|_| DecodeError::NotFound(raw.md5ext.clone()))?;
169                let mut buffer = Vec::new();
170                std::io::copy(&mut file, &mut buffer)?;
171                let wav_reader = hound::WavReader::new(&buffer[..]).map_err(|e| {
172                    DecodeError::InvalidData(format!("Failed to read WAV data: {}", e))
173                })?;
174                let spec = wav_reader.spec();
175                let samples: Vec<i16> = wav_reader
176                    .into_samples::<i16>()
177                    .collect::<Result<_, _>>()
178                    .map_err(|e| {
179                        DecodeError::InvalidData(format!("Failed to read WAV samples: {}", e))
180                    })?;
181                Ok((
182                    raw.name.clone(),
183                    Sound {
184                        data: samples,
185                        rate: spec.sample_rate,
186                    },
187                ))
188            })
189            .collect::<Result<HashMap<String, Sound>, DecodeError>>()?;
190        let lists = raw
191            .lists
192            .into_iter()
193            .map(|(_, raw_list)| {
194                (
195                    raw_list.0.clone(),
196                    List(raw_list.1.iter().map(|v| v.clone().into()).collect()),
197                )
198            })
199            .collect();
200
201        Ok(Self {
202            name: raw.name,
203            variables,
204            lists,
205            scripts: decode_scripts(&raw.blocks)?,
206            current_costume: raw.current_costume,
207            costumes,
208            sounds,
209            volume: raw.volume as f32 / 100.0,
210            layer_order: raw.layer_order,
211            visible: raw.visible,
212            position: (raw.x, raw.y),
213            size: raw.size as f32 / 100.0,
214            direction: raw.direction,
215            draggable: raw.draggable,
216            rotation_style: RotationStyle::try_from(raw.rotation_style.as_str())
217                .map_err(|_| DecodeError::InvalidData("Invalid rotation style".to_string()))?,
218        })
219    }
220
221    /// Returns a reference to a costume by its name, if it exists.
222    pub fn get_costume(&self, name: &str) -> Option<&Costume> {
223        self.costumes.get(name)
224    }
225
226    /// Returns a mutable reference to a costume by its name, if it exists.
227    pub fn get_costume_mut(&mut self, name: &str) -> Option<&mut Costume> {
228        self.costumes.get_mut(name)
229    }
230
231    /// Returns all the costume names in the sprite.
232    pub fn costume_names(&self) -> Vec<&String> {
233        self.costumes.keys().collect()
234    }
235
236    /// Returns all the costumes in the sprite.
237    pub fn costumes(&self) -> Vec<&Costume> {
238        self.costumes.values().collect()
239    }
240}
241
242/// The [`Stage`] struct represents the stage in a Scratch 3.0 project.
243#[derive(Debug, Clone)]
244pub struct Stage {
245    /// Contains all the global variables in the project.
246    pub variables: HashMap<String, Variable>,
247
248    // Contains all the global lists in the project.
249    pub lists: HashMap<String, List>,
250
251    /// Contains all the broadcasts in the project.
252    pub broadcasts: Vec<Broadcast>,
253
254    /// Contains all the scripts in the stage.
255    pub scripts: Vec<Script>,
256
257    /// The index of the current backdrop.
258    pub current_backdrop: usize,
259
260    /// Contains all the backdrops in the project.
261    backdrops: IndexMap<String, Costume>,
262
263    /// Contains all the sounds of the stage.
264    pub sounds: HashMap<String, Sound>,
265
266    /// The stage's volume (0.0 = 0%, 1.0 = 100%)
267    pub volume: f32,
268}
269
270impl Stage {
271    /// Returns a new stage from a [`RawStage`] and the zip archive.
272    pub fn new(
273        raw: RawStage,
274        zip: &mut ZipArchive<std::io::Cursor<Vec<u8>>>,
275    ) -> Result<Self, DecodeError> {
276        let variables = raw
277            .variables
278            .into_iter()
279            .map(|(_, raw_var)| (raw_var.name.clone(), Variable(raw_var.value.into())))
280            .collect();
281        #[cfg(feature = "costume_png")]
282        let backdrops = raw
283            .backdrops
284            .into_iter()
285            .map(|raw| {
286                let mut file = zip.by_name(&raw.md5ext)
287                    .map_err(|_| DecodeError::NotFound(raw.md5ext.clone()))?;
288                let mut buffer = Vec::new();
289                std::io::copy(&mut file, &mut buffer)?;
290                match raw.data_format.as_str() {
291                    "svg" => {
292                        let svg_data = std::str::from_utf8(&buffer).map_err(|_| {
293                            DecodeError::InvalidData("SVG data is not valid UTF-8".to_string())
294                        })?;
295                        let svg_tree =
296                            resvg::usvg::Tree::from_str(svg_data, &resvg::usvg::Options::default())
297                                .map_err(|e| {
298                                    DecodeError::InvalidData(format!("Failed to parse SVG: {}", e))
299                                })?;
300                        let image = crate::utils::rasterize_svg_to_png(
301                            &svg_tree,
302                            svg_tree.size().width() as u32,
303                            svg_tree.size().height() as u32,
304                        )
305                        .map_err(|e| {
306                            DecodeError::InvalidData(format!("Failed to rasterize SVG: {}", e))
307                        })?;
308                        Ok((raw.name, Costume(image)))
309                    }
310                    _ => {
311                        let image = image::load_from_memory(&buffer).map_err(|e| {
312                            DecodeError::InvalidData(format!("Failed to load image: {}", e))
313                        })?;
314                        Ok((raw.name, Costume(image)))
315                    }
316                }
317            })
318            .collect::<Result<IndexMap<String, Costume>, DecodeError>>()?;
319        #[cfg(feature = "costume_svg")]
320        let backdrops = raw
321            .backdrops
322            .into_iter()
323            .map(|raw| {
324                let mut file = zip.by_name(&raw.md5ext)
325                    .map_err(|_| DecodeError::NotFound(raw.md5ext.clone()))?;
326                let mut buffer = Vec::new();
327                std::io::copy(&mut file, &mut buffer)?;
328                match raw.data_format.as_str() {
329                    "svg" => {
330                        let svg_data = std::str::from_utf8(&buffer).map_err(|_| {
331                            DecodeError::InvalidData("SVG data is not valid UTF-8".to_string())
332                        })?;
333                        let svg_tree = usvg::Tree::from_str(svg_data, &usvg::Options::default())
334                            .map_err(|e| {
335                                DecodeError::InvalidData(format!("Failed to parse SVG: {}", e))
336                            })?;
337                        Ok((raw.name, Costume(svg_tree)))
338                    }
339                    _ => Err(DecodeError::InvalidData(format!(
340                        "Unsupported data format: {}",
341                        raw.data_format
342                    ))),
343                }
344            })
345            .collect::<Result<IndexMap<String, Costume>, DecodeError>>()?;
346        let sounds = raw
347            .sounds
348            .into_iter()
349            .map(|raw| {
350                let mut file = zip.by_name(&raw.md5ext)
351                    .map_err(|_| DecodeError::NotFound(raw.md5ext.clone()))?;
352                let mut buffer = Vec::new();
353                std::io::copy(&mut file, &mut buffer)?;
354                let wav_reader = hound::WavReader::new(&buffer[..]).map_err(|e| {
355                    DecodeError::InvalidData(format!("Failed to read WAV data: {}", e))
356                })?;
357                let spec = wav_reader.spec();
358                let samples: Vec<i16> = wav_reader
359                    .into_samples::<i16>()
360                    .collect::<Result<_, _>>()
361                    .map_err(|e| {
362                        DecodeError::InvalidData(format!("Failed to read WAV samples: {}", e))
363                    })?;
364                Ok((
365                    raw.name.clone(),
366                    Sound {
367                        data: samples,
368                        rate: spec.sample_rate,
369                    },
370                ))
371            })
372            .collect::<Result<HashMap<String, Sound>, DecodeError>>()?;
373        let lists = raw
374            .lists
375            .into_iter()
376            .map(|(_, raw_list)| {
377                (
378                    raw_list.0.clone(),
379                    List(raw_list.1.iter().map(|v| v.clone().into()).collect()),
380                )
381            })
382            .collect();
383
384        Ok(Self {
385            variables,
386            lists,
387            broadcasts: raw
388                .broadcasts
389                .into_iter()
390                .map(|(_, msg)| Broadcast(msg))
391                .collect(),
392            scripts: decode_scripts(&raw.blocks)?,
393            current_backdrop: raw.current_backdrop,
394            backdrops,
395            sounds,
396            volume: raw.volume as f32 / 100.0,
397        })
398    }
399
400    /// Returns a reference to a backdrop by its name, if it exists.
401    pub fn get_backdrop(&self, name: &str) -> Option<&Costume> {
402        self.backdrops.get(name)
403    }
404
405    /// Returns a mutable reference to a backdrop by its name, if it exists.
406    pub fn get_backdrop_mut(&mut self, name: &str) -> Option<&mut Costume> {
407        self.backdrops.get_mut(name)
408    }
409
410    /// Returns all the backdrop names in the stage.
411    pub fn backdrop_names(&self) -> Vec<&String> {
412        self.backdrops.keys().collect()
413    }
414
415    /// Returns all the backdrops in the stage.
416    pub fn backdrops(&self) -> Vec<&Costume> {
417        self.backdrops.values().collect()
418    }
419}
420
421/// The [`Broadcast`] struct represents a usable broadcast message in a Scratch 3.0 project.
422#[derive(Debug, Clone)]
423pub struct Broadcast(pub String);
424
425/// The [`RotationStyle`] enum represents the rotation style of a sprite in a Scratch 3.0 project.
426#[derive(Debug, Clone, Copy, PartialEq, Eq)]
427pub enum RotationStyle {
428    /// The sprite rotates freely.
429    AllAround,
430
431    /// The sprite rotates left and right.
432    LeftRight,
433
434    /// The sprite does not rotate.
435    DontRotate,
436}
437
438impl TryFrom<&str> for RotationStyle {
439    type Error = &'static str;
440
441    fn try_from(value: &str) -> Result<Self, Self::Error> {
442        match value {
443            "all around" => Ok(RotationStyle::AllAround),
444            "left-right" => Ok(RotationStyle::LeftRight),
445            "don't rotate" => Ok(RotationStyle::DontRotate),
446            _ => Err("Invalid rotation style"),
447        }
448    }
449}