diff --git a/experimental/Cargo.toml b/experimental/Cargo.toml index 490763b..424b4a0 100644 --- a/experimental/Cargo.toml +++ b/experimental/Cargo.toml @@ -10,6 +10,10 @@ publish = false [dependencies] cz = { path = "../cz/", features = ["png"] } +eframe = { version = "0.28.1", default-features = false, features = ["wayland", "x11", "accesskit", "default_fonts", "wgpu"] } +egui_extras = "0.28.1" +luca_pak = { path = "../luca_pak/" } +rfd = "0.14.1" [lints] workspace = true diff --git a/experimental/src/main.rs b/experimental/src/main.rs index 9dfd5d3..b473100 100644 --- a/experimental/src/main.rs +++ b/experimental/src/main.rs @@ -1,10 +1,157 @@ -use std::time::Instant; +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release -fn main() { - let mut cz_file = cz::open("test_file.cz3").unwrap(); - //cz_file.save_as_png("test.png").unwrap(); +use std::path::PathBuf; - cz_file.header_mut().set_version(4).unwrap(); +use eframe::{egui::{self, text::{LayoutJob, TextWrapping}, ColorImage, Image, Rgba, TextBuffer, TextureFilter, TextureHandle, TextureOptions}, epaint::Fonts}; +use luca_pak::{entry::EntryType, header, Pak}; - cz_file.save_as_cz("test_file.cz4").unwrap(); +fn main() -> eframe::Result { + let options = eframe::NativeOptions { + viewport: egui::ViewportBuilder::default().with_inner_size([1024.0, 800.0]), + follow_system_theme: true, + ..Default::default() + }; + eframe::run_native( + "LUCA PAK Explorer", + options, + Box::new(|cc| { + // This gives us image support: + egui_extras::install_image_loaders(&cc.egui_ctx); + + Ok(Box::::default()) + }), + ) +} + +struct PakExplorer { + open_file: Option, + selected_entry: Option, + image_texture: Option, + hex_string: Option>, +} + +impl Default for PakExplorer { + fn default() -> Self { + Self { + open_file: None, + selected_entry: None, + image_texture: None, + hex_string: None, + } + } +} + +impl eframe::App for PakExplorer { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + egui::CentralPanel::default().show(ctx, |ui| { + ctx.set_pixels_per_point(1.5); + ui.heading("PAK File Explorer"); + + if ui.button("Open file…").clicked() { + if let Some(path) = rfd::FileDialog::new().pick_file() { + let pak = Pak::open(&path).unwrap(); + self.open_file = Some(pak); + self.selected_entry = None; + self.image_texture = None; + self.hex_string = None; + } + } + + ui.separator(); + + if let Some(pak) = &self.open_file { + ui.label(format!("Opened {}", pak.path().file_name().unwrap().to_string_lossy())); + ui.label(format!("Contains {} Entries", pak.entries().len())); + + let selection = if let Some(entry) = &self.selected_entry { + entry.display_name() + } else { + "None".to_string() + }; + + egui::ComboBox::from_id_source("my-combobox") + .selected_text(selection) + .truncate() + .show_ui(ui, |ui| + { + ui.selectable_value(&mut self.selected_entry, None, ""); + for entry in pak.entries() { + if ui.selectable_value( + &mut self.selected_entry, + Some(entry.clone()), + entry.display_name(), + ).clicked() { + self.image_texture = None; + }; + } + }); + } else { + ui.centered_and_justified(|ui| + ui.label("No File Opened") + ); + } + + if let Some(entry) = &self.selected_entry { + if ui.button("Save entry…").clicked() { + if let Some(path) = rfd::FileDialog::new() + .set_file_name(entry.display_name()) + .save_file() + { + entry.save(&path).unwrap(); + } + } + match entry.file_type() { + EntryType::CZ0 | EntryType::CZ1 + | EntryType::CZ2 | EntryType::CZ3 + | EntryType::CZ4 | EntryType::CZ5 => + { + if ui.button("Save as PNG…").clicked() { + let mut display_name = entry.display_name(); + display_name.push_str(".png"); + if let Some(path) = rfd::FileDialog::new() + .set_file_name(display_name) + .save_file() + { + let cz = cz::DynamicCz::decode(&mut std::io::Cursor::new(entry.as_bytes())).unwrap(); + cz.save_as_png(&path).unwrap(); + } + } + + ui.separator(); + + let texture: &TextureHandle = self.image_texture.get_or_insert_with(|| { + let cz = cz::DynamicCz::decode(&mut std::io::Cursor::new(entry.as_bytes())).unwrap(); + let image = ColorImage::from_rgba_unmultiplied( + [cz.header().width() as usize, cz.header().height() as usize], + cz.as_raw() + ); + ui.ctx().load_texture("eventframe", image, TextureOptions { + magnification: TextureFilter::Nearest, + minification: TextureFilter::Linear, + ..Default::default() + }) + }); + + ui.centered_and_justified(|ui| + ui.add( + Image::from_texture(texture) + .show_loading_spinner(true) + .shrink_to_fit() + .rounding(2.0) + ) + ); + } + _ => { + ui.centered_and_justified(|ui| + ui.label("No Preview Available") + ); + }, + } + } else { + ui.centered_and_justified(|ui| + ui.label("Select an Entry") + ); + } + }); + } } diff --git a/luca_pak/src/entry.rs b/luca_pak/src/entry.rs index d9752b2..8a110ce 100644 --- a/luca_pak/src/entry.rs +++ b/luca_pak/src/entry.rs @@ -1,12 +1,9 @@ use std::{ - error::Error, - fs::File, - io::{BufWriter, Write}, - path::Path, + error::Error, fmt, fs::File, io::{BufWriter, Write}, path::Path }; /// A single file entry in a PAK file -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Entry { pub(super) index: usize, @@ -42,18 +39,6 @@ impl Entry { /// Save an [`Entry`] as its underlying data to a file pub fn save>(&self, path: &P) -> Result<(), Box> { - let mut path = path.as_ref().to_path_buf(); - if !path.is_dir() { - return Err("Path must be a directory".into()); - } - - // Save the file to + - if let Some(name) = &self.name { - path.push(name); - } else { - path.push(&self.id.to_string()) - } - let mut out_file = BufWriter::new(File::create(path)?); out_file.write_all(&self.data)?; @@ -70,4 +55,63 @@ impl Entry { pub fn as_bytes(&self) -> &Vec { &self.data } + + pub fn display_name(&self) -> String { + let mut name = self.name().clone().unwrap_or(self.id().to_string()); + let entry_type = self.file_type(); + name.push_str(&entry_type.extension()); + + name + } + + pub fn file_type(&self) -> EntryType { + if self.data[0..2] == [b'C', b'Z'] { + match self.data[2] { + b'0' => EntryType::CZ0, + b'1' => EntryType::CZ1, + b'2' => EntryType::CZ2, + b'3' => EntryType::CZ3, + b'4' => EntryType::CZ4, + b'5' => EntryType::CZ5, + _ => EntryType::Unknown, + } + } else if self.data[0..3] == [b'M', b'V', b'T'] { + EntryType::MVT + } else { + EntryType::Unknown + } + } +} + + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EntryType { + CZ0, + CZ1, + CZ2, + CZ3, + CZ4, + CZ5, + + /// An MVT video file + MVT, + + /// Who knows! + Unknown, +} + +impl EntryType { + /// Get the file extension for the file + pub fn extension(&self) -> &'static str { + match self { + Self::CZ0 => ".cz0", + Self::CZ1 => ".cz1", + Self::CZ2 => ".cz2", + Self::CZ3 => ".cz3", + Self::CZ4 => ".cz4", + Self::CZ5 => ".cz5", + Self::MVT => ".mvt", + Self::Unknown => "", + } + } } diff --git a/luca_pak/src/lib.rs b/luca_pak/src/lib.rs index 951e39c..9222a27 100644 --- a/luca_pak/src/lib.rs +++ b/luca_pak/src/lib.rs @@ -1,5 +1,5 @@ -mod entry; -mod header; +pub mod entry; +pub mod header; use byteorder::{LittleEndian, ReadBytesExt}; use header::Header; diff --git a/luca_pak/src/main.rs b/luca_pak/src/main.rs index 44a0473..fd966b2 100644 --- a/luca_pak/src/main.rs +++ b/luca_pak/src/main.rs @@ -68,7 +68,9 @@ fn main() { } for entry in pak.entries() { - entry.save(&output).unwrap(); + let mut outpath = output.clone(); + outpath.push(entry.display_name()); + entry.save(&outpath).unwrap(); } }, Commands::Replace { batch, name, id, replacement, output } => {