use std::cmp::max;
use std::fs;
use std::fs::File;
use std::io::{Read, Write};
use std::path::{Path, PathBuf, MAIN_SEPARATOR};
use directories::UserDirs;
use zip::write::SimpleFileOptions;
use zip::{ZipArchive, ZipWriter};
use crate::image::{Color, Image, Rgb24};
use crate::pyxel::Pyxel;
use crate::resource_data::ResourceData;
use crate::screencast::Screencast;
use crate::settings::{
BASE_DIR, DEFAULT_CAPTURE_SCALE, DEFAULT_CAPTURE_SEC, PALETTE_FILE_EXTENSION,
RESOURCE_ARCHIVE_NAME, RESOURCE_FILE_EXTENSION, RESOURCE_FORMAT_VERSION,
};
pub struct Resource {
capture_scale: u32,
screencast: Screencast,
}
impl Resource {
pub fn new(capture_scale: Option<u32>, capture_sec: Option<u32>, fps: u32) -> Self {
let capture_scale = capture_scale.unwrap_or(DEFAULT_CAPTURE_SCALE);
let capture_sec = capture_sec.unwrap_or(DEFAULT_CAPTURE_SEC);
Self {
capture_scale: max(capture_scale, 1),
screencast: Screencast::new(fps, capture_sec),
}
}
}
impl Pyxel {
pub fn load(
&mut self,
filename: &str,
exclude_images: Option<bool>,
exclude_tilemaps: Option<bool>,
exclude_sounds: Option<bool>,
exclude_musics: Option<bool>,
) -> Result<(), String> {
let file = File::open(Path::new(&filename))
.map_err(|_e| format!("Failed to open file '{filename}'"))?;
let mut archive = ZipArchive::new(file).unwrap();
if archive.by_name("pyxel_resource/version").is_ok() {
println!("An old Pyxel resource file '{filename}' is loaded. Please re-save it with the latest Pyxel.");
self.load_old_resource(
&mut archive,
filename,
!exclude_images.unwrap_or(false),
!exclude_tilemaps.unwrap_or(false),
!exclude_sounds.unwrap_or(false),
!exclude_musics.unwrap_or(false),
);
self.load_pal(filename);
return Ok(());
}
let mut file = archive.by_name(RESOURCE_ARCHIVE_NAME).unwrap();
let mut toml_text = String::new();
file.read_to_string(&mut toml_text).unwrap();
let format_version = Self::parse_format_version(&toml_text);
if format_version > RESOURCE_FORMAT_VERSION {
Err(format!("Unknown resource file version '{format_version}'"))
} else {
let resource_data = ResourceData::from_toml(&toml_text);
resource_data.to_runtime(
self,
exclude_images.unwrap_or(false),
exclude_tilemaps.unwrap_or(false),
exclude_sounds.unwrap_or(false),
exclude_musics.unwrap_or(false),
);
self.load_pal(filename);
Ok(())
}
}
pub fn save(
&mut self,
filename: &str,
exclude_images: Option<bool>,
exclude_tilemaps: Option<bool>,
exclude_sounds: Option<bool>,
exclude_musics: Option<bool>,
) -> Result<(), String> {
let toml_text = ResourceData::from_runtime(self).to_toml(
exclude_images.unwrap_or(false),
exclude_tilemaps.unwrap_or(false),
exclude_sounds.unwrap_or(false),
exclude_musics.unwrap_or(false),
);
let path = std::path::Path::new(&filename);
let file = std::fs::File::create(path)
.map_err(|_e| format!("Failed to open file '{filename}'"))?;
let mut zip = ZipWriter::new(file);
zip.start_file(RESOURCE_ARCHIVE_NAME, SimpleFileOptions::default())
.unwrap();
zip.write_all(toml_text.as_bytes()).unwrap();
zip.finish().unwrap();
pyxel_platform::export_browser_file(filename);
Ok(())
}
pub fn load_pal(&mut self, filename: &str) {
let filename = Self::palette_filename(filename);
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() = if colors.is_empty() {
vec![0xffffff]
} else {
colors
};
}
}
pub fn save_pal(&self, filename: &str) -> Result<(), String> {
let filename = Self::palette_filename(filename);
let mut file = File::create(Path::new(&filename))
.map_err(|_e| format!("Failed to open file '{filename}'"))?;
let colors = self.colors.lock();
for &color in colors.iter() {
writeln!(file, "{color:06x}").unwrap();
}
pyxel_platform::export_browser_file(&filename);
Ok(())
}
pub fn screenshot(&mut self, scale: Option<u32>) {
let filename = Self::prepend_desktop_path(&format!("pyxel-{}", Self::datetime_string()));
let scale = max(scale.unwrap_or(self.resource.capture_scale), 1);
self.screen.lock().save(&filename, scale).unwrap();
pyxel_platform::export_browser_file(&(filename + ".png"));
}
pub fn screencast(&mut self, scale: Option<u32>) {
let filename = Self::prepend_desktop_path(&format!("pyxel-{}", Self::datetime_string()));
let scale = max(scale.unwrap_or(self.resource.capture_scale), 1);
self.resource.screencast.save(&filename, scale);
pyxel_platform::export_browser_file(&(filename + ".gif"));
}
pub fn reset_screencast(&mut self) {
self.resource.screencast.reset();
}
pub fn user_data_dir(&self, vendor_name: &str, app_name: &str) -> String {
let home_dir = UserDirs::new()
.map_or_else(PathBuf::new, |user_dirs| user_dirs.home_dir().to_path_buf());
let app_data_dir = home_dir
.join(BASE_DIR)
.join(Self::make_dir_name(vendor_name))
.join(Self::make_dir_name(app_name));
if !app_data_dir.exists() {
fs::create_dir_all(&app_data_dir).unwrap();
println!("created '{}'", app_data_dir.to_string_lossy());
}
let mut app_data_dir = app_data_dir.to_string_lossy().to_string();
if !app_data_dir.ends_with(MAIN_SEPARATOR) {
app_data_dir.push(MAIN_SEPARATOR);
}
app_data_dir
}
pub(crate) fn capture_screen(&mut self) {
self.resource.screencast.capture(
self.width,
self.height,
&self.screen.lock().canvas.data,
&self.colors.lock(),
self.frame_count,
);
}
pub(crate) fn dump_image_bank(&self, image_index: u32) {
let filename = Self::prepend_desktop_path(&format!("pyxel-image{image_index}"));
if let Some(image) = self.images.lock().get(image_index as usize) {
image.lock().save(&filename, 1).unwrap();
pyxel_platform::export_browser_file(&(filename + ".png"));
}
}
pub(crate) fn dump_palette(&self) {
let filename = Self::prepend_desktop_path("pyxel-palette");
let num_colors = self.colors.lock().len();
let image = Image::new(num_colors as u32, 1);
{
let mut image = image.lock();
for i in 0..num_colors {
image.pset(i as f32, 0.0, i as Color);
}
image.save(&filename, 16).unwrap();
pyxel_platform::export_browser_file(&(filename + ".png"));
}
}
fn palette_filename(filename: &str) -> String {
if filename.to_lowercase().ends_with(RESOURCE_FILE_EXTENSION) {
filename[..filename.len() - RESOURCE_FILE_EXTENSION.len()].to_string()
+ PALETTE_FILE_EXTENSION
} else {
filename.to_string()
}
}
fn datetime_string() -> String {
chrono::Local::now().format("%Y%m%d-%H%M%S").to_string()
}
fn prepend_desktop_path(basename: &str) -> String {
let desktop_dir = UserDirs::new()
.and_then(|user_dirs| user_dirs.desktop_dir().map(Path::to_path_buf))
.unwrap_or_default();
desktop_dir.join(basename).to_string_lossy().to_string()
}
fn parse_format_version(toml_text: &str) -> u32 {
toml_text
.lines()
.find(|line| line.trim().starts_with("format_version"))
.and_then(|line| line.split_once('='))
.map(|(_, value)| value.trim().parse::<u32>())
.unwrap()
.unwrap()
}
fn make_dir_name(name: &str) -> String {
name.to_lowercase()
.replace(' ', "_")
.chars()
.filter(|c| c.is_alphanumeric() || *c == '_' || *c == '-')
.collect()
}
}