From 50c2cf4686b53ebd2b76318223096660e92305a4 Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Mon, 22 Jun 2026 00:35:19 +0400 Subject: chore: remove Python tooling and resource viewer --- apps/resource-viewer/src/main.rs | 518 --------------------------------------- 1 file changed, 518 deletions(-) delete mode 100644 apps/resource-viewer/src/main.rs (limited to 'apps/resource-viewer/src/main.rs') diff --git a/apps/resource-viewer/src/main.rs b/apps/resource-viewer/src/main.rs deleted file mode 100644 index 508c407..0000000 --- a/apps/resource-viewer/src/main.rs +++ /dev/null @@ -1,518 +0,0 @@ -use iced::widget::{button, column, container, horizontal_space, row, scrollable, text}; -use iced::{application, Element, Length, Task, Theme}; -use rfd::FileDialog; -use std::collections::BTreeMap; -use std::fmt::Write as _; -use std::fs; -use std::path::{Path, PathBuf}; - -fn main() -> iced::Result { - application("Parkan Resource Viewer", update, view) - .theme(theme) - .run_with(|| (ViewerApp::default(), Task::none())) -} - -fn theme(_state: &ViewerApp) -> Theme { - Theme::Light -} - -#[derive(Debug, Default)] -struct ViewerApp { - document: Option, - status: String, -} - -#[derive(Debug, Clone)] -enum Message { - OpenRequested, - SelectNode(Selection), -} - -fn update(state: &mut ViewerApp, message: Message) -> Task { - match message { - Message::OpenRequested => { - if let Some(path) = pick_archive_file() { - match load_document(&path) { - Ok(document) => { - state.status = - format!("Loaded {} as {}", path.display(), document.format.label()); - state.document = Some(document); - } - Err(err) => { - state.status = err; - } - } - } - } - Message::SelectNode(selection) => { - if let Some(document) = state.document.as_mut() { - document.selected = selection; - } - } - } - - Task::none() -} - -fn view(state: &ViewerApp) -> Element<'_, Message> { - let top_bar = row![ - button("Open archive").on_press(Message::OpenRequested), - text(status_text(state)).size(14) - ] - .spacing(12); - - let content = if let Some(document) = &state.document { - view_document(document) - } else { - container(text("Open an .nres/.rsli/.lib archive to start.").size(16)) - .width(Length::Fill) - .height(Length::Fill) - .center_x(Length::Fill) - .center_y(Length::Fill) - .into() - }; - - container(column![top_bar, content].spacing(12).padding(12)) - .width(Length::Fill) - .height(Length::Fill) - .into() -} - -fn status_text(state: &ViewerApp) -> String { - if state.status.is_empty() { - String::from("Ready") - } else { - state.status.clone() - } -} - -fn view_document(document: &DocumentModel) -> Element<'_, Message> { - let mut tree = column![text("Archive tree").size(18)].spacing(6); - for item in &document.tree_rows { - let indent = horizontal_space().width(Length::Fixed(f32::from(item.depth) * 16.0)); - - let line = row![indent, text(&item.label).size(14)].spacing(6); - if let Some(selection) = item.selection { - let mut node_button = button(line) - .width(Length::Fill) - .on_press(Message::SelectNode(selection)); - - if selection == document.selected { - node_button = node_button.style(button::primary); - } - - tree = tree.push(node_button); - } else { - tree = tree.push(line); - } - } - - let (panel_title, fields) = selected_fields(document); - let mut fields_column = column![text(panel_title).size(18)].spacing(8); - - for field in fields { - fields_column = fields_column.push( - row![ - text(&field.key).size(14).width(Length::Fixed(220.0)), - text(&field.value).size(14).width(Length::Fill) - ] - .spacing(12), - ); - } - - let left = container(scrollable(tree)) - .width(Length::FillPortion(2)) - .height(Length::Fill); - - let right = container(scrollable(fields_column)) - .width(Length::FillPortion(5)) - .height(Length::Fill); - - row![left, right].spacing(12).height(Length::Fill).into() -} - -fn selected_fields(document: &DocumentModel) -> (String, &[FieldRow]) { - match document.selected { - Selection::Archive => ( - format!( - "{} fields ({})", - document.format.label(), - document.path.display() - ), - &document.archive_fields, - ), - Selection::Entry(index) => { - if let Some(entry) = document.entries.get(index) { - (entry.panel_title.clone(), &entry.fields) - } else { - (String::from("Entry"), &[]) - } - } - } -} - -fn pick_archive_file() -> Option { - FileDialog::new() - .set_title("Open Parkan archive") - .pick_file() -} - -fn load_document(path: &Path) -> Result { - let bytes = - fs::read(path).map_err(|err| format!("Failed to read {}: {err}", path.display()))?; - let Some(format) = detect_archive_format(&bytes) else { - return Err(format!( - "{} is not recognized as NRes/RsLi (unsupported magic).", - path.display() - )); - }; - - match format { - ArchiveFormat::Nres => load_nres_document(path), - ArchiveFormat::Rsli => load_rsli_document(path), - } -} - -fn detect_archive_format(bytes: &[u8]) -> Option { - if bytes.len() >= 4 && &bytes[0..4] == b"NRes" { - return Some(ArchiveFormat::Nres); - } - - if bytes.len() >= 2 && &bytes[0..2] == b"NL" { - return Some(ArchiveFormat::Rsli); - } - - None -} - -fn load_nres_document(path: &Path) -> Result { - let archive = nres::Archive::open_path(path) - .map_err(|err| format!("NRes open failed for {}: {err}", path.display()))?; - - let info = archive.info(); - let mut archive_fields = vec![ - FieldRow::new("format", "NRes"), - FieldRow::new("file_size", info.file_size.to_string()), - FieldRow::new("raw_mode", info.raw_mode.to_string()), - ]; - - if let Some(header) = &info.header { - archive_fields.push(FieldRow::new( - "magic", - String::from_utf8_lossy(&header.magic).into_owned(), - )); - archive_fields.push(FieldRow::new("version", format_u32_dec_hex(header.version))); - archive_fields.push(FieldRow::new("entry_count", header.entry_count.to_string())); - archive_fields.push(FieldRow::new( - "total_size", - format!("{} (0x{:08X})", header.total_size, header.total_size), - )); - archive_fields.push(FieldRow::new( - "directory_offset", - header.directory_offset.to_string(), - )); - archive_fields.push(FieldRow::new( - "directory_size", - header.directory_size.to_string(), - )); - } - - let mut entries = Vec::new(); - for entry in archive.entries_inspect() { - let meta = entry.meta; - let mut fields = vec![ - FieldRow::new("id", entry.id.0.to_string()), - FieldRow::new("name", meta.name.clone()), - FieldRow::new("type_id", format_u32_dec_hex(meta.kind)), - FieldRow::new("attr1", format_u32_dec_hex(meta.attr1)), - FieldRow::new("attr2", format_u32_dec_hex(meta.attr2)), - FieldRow::new("attr3", format_u32_dec_hex(meta.attr3)), - FieldRow::new("data_offset", meta.data_offset.to_string()), - FieldRow::new("data_size", meta.data_size.to_string()), - FieldRow::new("sort_index", meta.sort_index.to_string()), - FieldRow::new("name_raw_hex", bytes_as_hex(entry.name_raw)), - FieldRow::new("name_raw_ascii", bytes_as_ascii(entry.name_raw)), - ]; - - fields.push(FieldRow::new("find_key", meta.name.to_ascii_lowercase())); - - entries.push(EntryView { - full_name: meta.name.clone(), - panel_title: format!("NRes entry #{}: {}", entry.id.0, meta.name), - fields, - }); - } - - let tree_rows = build_tree_rows(&entries); - - Ok(DocumentModel { - path: path.to_path_buf(), - format: ArchiveFormat::Nres, - archive_fields, - entries, - tree_rows, - selected: Selection::Archive, - }) -} - -fn load_rsli_document(path: &Path) -> Result { - let library = rsli::Library::open_path(path) - .map_err(|err| format!("RsLi open failed for {}: {err}", path.display()))?; - - let header = library.header(); - let mut archive_fields = vec![ - FieldRow::new("format", "RsLi"), - FieldRow::new("magic", String::from_utf8_lossy(&header.magic).into_owned()), - FieldRow::new( - "reserved", - format!("{} (0x{:02X})", header.reserved, header.reserved), - ), - FieldRow::new( - "version", - format!("{} (0x{:02X})", header.version, header.version), - ), - FieldRow::new("entry_count", header.entry_count.to_string()), - FieldRow::new("presorted_flag", format!("0x{:04X}", header.presorted_flag)), - FieldRow::new("xor_seed", format!("0x{:08X}", header.xor_seed)), - FieldRow::new("header_raw_hex", bytes_as_hex(&header.raw)), - ]; - - if let Some(ao) = library.ao_trailer() { - archive_fields.push(FieldRow::new("ao_trailer", "present")); - archive_fields.push(FieldRow::new("ao_overlay", ao.overlay.to_string())); - archive_fields.push(FieldRow::new("ao_raw_hex", bytes_as_hex(&ao.raw))); - } else { - archive_fields.push(FieldRow::new("ao_trailer", "absent")); - } - - let mut entries = Vec::new(); - for entry in library.entries_inspect() { - let meta = entry.meta; - let method_raw = (meta.flags as u16 as u32) & 0x1E0; - - let fields = vec![ - FieldRow::new("id", entry.id.0.to_string()), - FieldRow::new("name", meta.name.clone()), - FieldRow::new( - "flags", - format!("{} (0x{:04X})", meta.flags, meta.flags as u16), - ), - FieldRow::new("method", format!("{:?}", meta.method)), - FieldRow::new("method_raw", format!("0x{:03X}", method_raw)), - FieldRow::new("packed_size", meta.packed_size.to_string()), - FieldRow::new("unpacked_size", meta.unpacked_size.to_string()), - FieldRow::new("data_offset_effective", meta.data_offset.to_string()), - FieldRow::new("data_offset_raw", entry.data_offset_raw.to_string()), - FieldRow::new("sort_to_original", entry.sort_to_original.to_string()), - FieldRow::new("name_raw_hex", bytes_as_hex(entry.name_raw)), - FieldRow::new("name_raw_ascii", bytes_as_ascii(entry.name_raw)), - FieldRow::new("service_tail_hex", bytes_as_hex(entry.service_tail)), - FieldRow::new("service_tail_ascii", bytes_as_ascii(entry.service_tail)), - ]; - - entries.push(EntryView { - full_name: meta.name.clone(), - panel_title: format!("RsLi entry #{}: {}", entry.id.0, meta.name), - fields, - }); - } - - let tree_rows = build_tree_rows(&entries); - - Ok(DocumentModel { - path: path.to_path_buf(), - format: ArchiveFormat::Rsli, - archive_fields, - entries, - tree_rows, - selected: Selection::Archive, - }) -} - -fn build_tree_rows(entries: &[EntryView]) -> Vec { - let mut root = FolderNode::default(); - for (index, entry) in entries.iter().enumerate() { - insert_tree_path(&mut root, &entry.full_name, index); - } - - let mut rows = vec![TreeRow { - depth: 0, - label: String::from("[Archive fields]"), - selection: Some(Selection::Archive), - }]; - - flatten_tree(&root, 0, &mut rows); - rows -} - -fn insert_tree_path(root: &mut FolderNode, full_name: &str, entry_index: usize) { - let mut parts: Vec<&str> = full_name - .split(['/', '\\']) - .filter(|part| !part.is_empty()) - .collect(); - - if parts.is_empty() { - parts.push(full_name); - } - - if parts.len() == 1 { - root.files.push((parts[0].to_string(), entry_index)); - return; - } - - let file_name = parts.pop().unwrap_or(full_name); - let mut node = root; - for part in parts { - node = node.folders.entry(part.to_string()).or_default(); - } - - node.files.push((file_name.to_string(), entry_index)); -} - -fn flatten_tree(node: &FolderNode, depth: u16, out: &mut Vec) { - for (folder_name, folder_node) in &node.folders { - out.push(TreeRow { - depth, - label: format!("{folder_name}/"), - selection: None, - }); - flatten_tree(folder_node, depth.saturating_add(1), out); - } - - let mut files = node.files.clone(); - files.sort_by(|left, right| left.0.cmp(&right.0)); - - for (name, index) in files { - out.push(TreeRow { - depth, - label: name, - selection: Some(Selection::Entry(index)), - }); - } -} - -fn bytes_as_hex(bytes: &[u8]) -> String { - let mut out = String::new(); - for (index, byte) in bytes.iter().enumerate() { - if index > 0 { - out.push(' '); - } - let _ = write!(&mut out, "{byte:02X}"); - } - out -} - -fn bytes_as_ascii(bytes: &[u8]) -> String { - bytes - .iter() - .map(|byte| { - if byte.is_ascii_graphic() || *byte == b' ' { - char::from(*byte) - } else { - '.' - } - }) - .collect() -} - -fn format_u32_dec_hex(value: u32) -> String { - format!("{} (0x{:08X})", value, value) -} - -#[derive(Debug, Clone)] -struct DocumentModel { - path: PathBuf, - format: ArchiveFormat, - archive_fields: Vec, - entries: Vec, - tree_rows: Vec, - selected: Selection, -} - -#[derive(Debug, Clone, Copy)] -enum ArchiveFormat { - Nres, - Rsli, -} - -impl ArchiveFormat { - fn label(self) -> &'static str { - match self { - Self::Nres => "NRes", - Self::Rsli => "RsLi", - } - } -} - -#[derive(Debug, Clone)] -struct EntryView { - full_name: String, - panel_title: String, - fields: Vec, -} - -#[derive(Debug, Clone)] -struct FieldRow { - key: String, - value: String, -} - -impl FieldRow { - fn new(key: impl Into, value: impl Into) -> Self { - Self { - key: key.into(), - value: value.into(), - } - } -} - -#[derive(Debug, Clone)] -struct TreeRow { - depth: u16, - label: String, - selection: Option, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum Selection { - Archive, - Entry(usize), -} - -#[derive(Default, Debug)] -struct FolderNode { - folders: BTreeMap, - files: Vec<(String, usize)>, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn tree_builds_nested_paths() { - let entries = vec![ - EntryView { - full_name: String::from("textures/ui/hud.texm"), - panel_title: String::new(), - fields: vec![], - }, - EntryView { - full_name: String::from("textures/world/ground.texm"), - panel_title: String::new(), - fields: vec![], - }, - EntryView { - full_name: String::from("root_file.msh"), - panel_title: String::new(), - fields: vec![], - }, - ]; - - let rows = build_tree_rows(&entries); - assert!(rows.iter().any(|row| row.label == "textures/")); - assert!(rows.iter().any(|row| row.label == "ui/")); - assert!(rows.iter().any(|row| row.label == "hud.texm")); - assert!(rows.iter().any(|row| row.label == "root_file.msh")); - } -} -- cgit v1.2.3