pyxel-engine 2.6.8

Core engine for Pyxel, a retro game engine for Python
use std::fmt;
use std::fs::File;
use std::io::Read;
use std::path::Path;

use zip::ZipArchive;

use crate::image::{Color, Image, Rgb24};
use crate::music::Music;
use crate::pyxel::Pyxel;
use crate::settings::{
    DEFAULT_SOUND_SPEED, NUM_CHANNELS, NUM_IMAGES, NUM_MUSICS, NUM_SOUNDS, NUM_TILEMAPS,
    PALETTE_FILE_EXTENSION, TILEMAP_SIZE, VERSION,
};
use crate::sound::{Sound, SoundEffect, SoundNote, SoundTone, SoundVolume};
use crate::tilemap::{ImageSource, ImageTileCoord, Tilemap};
use crate::utils::{parse_hex_string, simplify_string};

pub const RESOURCE_ARCHIVE_DIRNAME: &str = "pyxel_resource/";

trait ResourceItem {
    fn resource_name(item_index: u32) -> String;
    fn clear(&mut self);
    fn deserialize(&mut self, version: u32, input: &str);
}

impl ResourceItem for Image {
    fn resource_name(item_index: u32) -> String {
        RESOURCE_ARCHIVE_DIRNAME.to_string() + "image" + &item_index.to_string()
    }

    fn clear(&mut self) {
        self.cls(0);
    }

    fn deserialize(&mut self, _version: u32, input: &str) {
        for (i, line) in input.lines().enumerate() {
            string_loop!(j, color, line, 1, {
                self.canvas
                    .write_data(j, i, parse_hex_string(&color).unwrap() as Color);
            });
        }
    }
}

impl fmt::Display for ImageSource {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ImageSource::Index(index) => write!(f, "{index}"),
            ImageSource::Image(_) => write!(f, "0"),
        }
    }
}

impl ResourceItem for Tilemap {
    fn resource_name(item_index: u32) -> String {
        RESOURCE_ARCHIVE_DIRNAME.to_string() + "tilemap" + &item_index.to_string()
    }

    fn clear(&mut self) {
        self.cls((0, 0));
    }

    fn deserialize(&mut self, version: u32, input: &str) {
        for (y, line) in input.lines().enumerate() {
            if y < TILEMAP_SIZE as usize {
                if version < 10500 {
                    string_loop!(x, tile, line, 3, {
                        let tile = parse_hex_string(&tile).unwrap();
                        self.canvas.write_data(
                            x,
                            y,
                            ((tile % 32) as ImageTileCoord, (tile / 32) as ImageTileCoord),
                        );
                    });
                } else {
                    string_loop!(x, tile, line, 4, {
                        let tile_x = parse_hex_string(&tile[0..2]).unwrap();
                        let tile_y = parse_hex_string(&tile[2..4]).unwrap();
                        self.canvas.write_data(
                            x,
                            y,
                            (tile_x as ImageTileCoord, tile_y as ImageTileCoord),
                        );
                    });
                }
            } else {
                self.imgsrc = ImageSource::Index(line.parse::<usize>().unwrap() as u32);
            }
        }
    }
}

impl ResourceItem for Sound {
    fn resource_name(item_index: u32) -> String {
        RESOURCE_ARCHIVE_DIRNAME.to_string() + "sound" + &format!("{item_index:02}")
    }

    fn clear(&mut self) {
        self.notes.clear();
        self.tones.clear();
        self.volumes.clear();
        self.effects.clear();
        self.speed = DEFAULT_SOUND_SPEED;
    }

    fn deserialize(&mut self, _version: u32, input: &str) {
        self.clear();

        for (i, line) in input.lines().enumerate() {
            if line == "none" {
                continue;
            }

            if i == 0 {
                string_loop!(j, value, line, 2, {
                    self.notes
                        .push(parse_hex_string(&value).unwrap() as i8 as SoundNote);
                });
            } else if i == 1 {
                string_loop!(j, value, line, 1, {
                    self.tones
                        .push(parse_hex_string(&value).unwrap() as SoundTone);
                });
            } else if i == 2 {
                string_loop!(j, value, line, 1, {
                    self.volumes
                        .push(parse_hex_string(&value).unwrap() as SoundVolume);
                });
            } else if i == 3 {
                string_loop!(j, value, line, 1, {
                    self.effects
                        .push(parse_hex_string(&value).unwrap() as SoundEffect);
                });
            } else if i == 4 {
                self.speed = line.parse().unwrap();
            }
        }
    }
}

impl ResourceItem for Music {
    fn resource_name(item_index: u32) -> String {
        RESOURCE_ARCHIVE_DIRNAME.to_string() + "music" + &item_index.to_string()
    }

    fn clear(&mut self) {
        self.seqs = (0..NUM_CHANNELS)
            .map(|_| new_shared_type!(Vec::new()))
            .collect();
    }

    fn deserialize(&mut self, _version: u32, input: &str) {
        self.clear();

        for (i, line) in input.lines().enumerate() {
            if line == "none" {
                continue;
            }
            string_loop!(j, value, line, 2, {
                self.seqs[i].lock().push(parse_hex_string(&value).unwrap());
            });
        }
    }
}

impl Pyxel {
    pub fn load_old_resource(
        &mut self,
        archive: &mut ZipArchive<File>,
        filename: &str,
        include_images: bool,
        include_tilemaps: bool,
        include_sounds: bool,
        include_musics: bool,
    ) {
        let version_name = RESOURCE_ARCHIVE_DIRNAME.to_string() + "version";
        let contents = {
            let mut file = archive.by_name(&version_name).unwrap();
            let mut contents = String::new();
            file.read_to_string(&mut contents).unwrap();
            contents
        };
        let version = parse_version_string(&contents).unwrap();
        assert!(
            version <= parse_version_string(VERSION).unwrap(),
            "Unsupported resource file version '{contents}'"
        );

        macro_rules! deserialize {
            ($type: ty, $list: ident, $count: expr) => {
                for i in 0..$count {
                    if let Ok(mut file) = archive.by_name(&<$type>::resource_name(i)) {
                        let mut input = String::new();
                        file.read_to_string(&mut input).unwrap();
                        self.$list.lock()[i as usize]
                            .lock()
                            .deserialize(version, &input);
                    } else {
                        self.$list.lock()[i as usize].lock().clear();
                    }
                }
            };
        }

        if include_images {
            deserialize!(Image, images, NUM_IMAGES);
        }
        if include_tilemaps {
            deserialize!(Tilemap, tilemaps, NUM_TILEMAPS);
        }
        if include_sounds {
            deserialize!(Sound, sounds, NUM_SOUNDS);
        }
        if include_musics {
            deserialize!(Music, musics, NUM_MUSICS);
        }

        // Try to load Pyxel palette file
        let filename = filename
            .rfind('.')
            .map_or(filename, |i| &filename[..i])
            .to_string()
            + PALETTE_FILE_EXTENSION;

        if let Ok(mut file) = File::open(Path::new(&filename)) {
            let mut contents = String::new();
            file.read_to_string(&mut contents).unwrap();

            let colors: Vec<Rgb24> = contents
                .replace("\r\n", "\n")
                .replace('\r', "\n")
                .split('\n')
                .filter(|s| !s.is_empty())
                .map(|s| u32::from_str_radix(s.trim(), 16).unwrap() as Rgb24)
                .collect();

            self.colors.lock().clear();
            self.colors.lock().extend(colors.iter());
        }
    }
}

fn parse_version_string(string: &str) -> Result<u32, &str> {
    let mut version = 0;

    for (i, number) in simplify_string(string).split('.').enumerate() {
        let digit = number.len();
        let number = if i > 0 && digit == 1 {
            "0".to_string() + number
        } else if i == 0 || digit == 2 {
            number.to_string()
        } else {
            return Err("invalid version string");
        };

        if let Ok(number) = number.parse::<u32>() {
            version = version * 100 + number;
        } else {
            return Err("invalid version string");
        }
    }

    Ok(version)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_version_string() {
        assert_eq!(parse_version_string("1.2.3"), Ok(10203));
        assert_eq!(parse_version_string("12.34.5"), Ok(123405));
        assert_eq!(parse_version_string("12.3.04"), Ok(120304));
        assert_eq!(
            parse_version_string("12.345.0"),
            Err("invalid version string")
        );
        assert_eq!(
            parse_version_string("12.0.345"),
            Err("invalid version string")
        );
        assert_eq!(parse_version_string(" "), Err("invalid version string"));
    }
}