From 8d8653133bf3a12ac58c0e4f34624e9beac11751 Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Sat, 8 Feb 2025 01:11:02 +0000 Subject: Обновление структуры проекта MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 9 -- Cargo.toml | 8 +- libnres/Cargo.toml | 16 --- libnres/README.md | 25 ----- libnres/src/converter.rs | 33 ------ libnres/src/error.rs | 45 -------- libnres/src/lib.rs | 24 ---- libnres/src/reader.rs | 227 -------------------------------------- libs/nres/Cargo.toml | 10 ++ libs/nres/src/converter.rs | 33 ++++++ libs/nres/src/error.rs | 45 ++++++++ libs/nres/src/lib.rs | 24 ++++ libs/nres/src/reader.rs | 227 ++++++++++++++++++++++++++++++++++++++ nres-cli/Cargo.toml | 20 ---- nres-cli/README.md | 6 - nres-cli/src/main.rs | 198 --------------------------------- texture-decoder/Cargo.toml | 8 -- texture-decoder/README.md | 13 --- texture-decoder/src/main.rs | 41 ------- tools/nres-cli/Cargo.toml | 14 +++ tools/nres-cli/README.md | 6 + tools/nres-cli/src/main.rs | 198 +++++++++++++++++++++++++++++++++ tools/texture-decoder/Cargo.toml | 8 ++ tools/texture-decoder/README.md | 13 +++ tools/texture-decoder/src/main.rs | 41 +++++++ tools/unpacker/Cargo.toml | 9 ++ tools/unpacker/README.md | 41 +++++++ tools/unpacker/src/main.rs | 124 +++++++++++++++++++++ unpacker/Cargo.toml | 9 -- unpacker/README.md | 41 ------- unpacker/src/main.rs | 124 --------------------- 31 files changed, 794 insertions(+), 846 deletions(-) delete mode 100644 libnres/Cargo.toml delete mode 100644 libnres/README.md delete mode 100644 libnres/src/converter.rs delete mode 100644 libnres/src/error.rs delete mode 100644 libnres/src/lib.rs delete mode 100644 libnres/src/reader.rs create mode 100644 libs/nres/Cargo.toml create mode 100644 libs/nres/src/converter.rs create mode 100644 libs/nres/src/error.rs create mode 100644 libs/nres/src/lib.rs create mode 100644 libs/nres/src/reader.rs delete mode 100644 nres-cli/Cargo.toml delete mode 100644 nres-cli/README.md delete mode 100644 nres-cli/src/main.rs delete mode 100644 texture-decoder/Cargo.toml delete mode 100644 texture-decoder/README.md delete mode 100644 texture-decoder/src/main.rs create mode 100644 tools/nres-cli/Cargo.toml create mode 100644 tools/nres-cli/README.md create mode 100644 tools/nres-cli/src/main.rs create mode 100644 tools/texture-decoder/Cargo.toml create mode 100644 tools/texture-decoder/README.md create mode 100644 tools/texture-decoder/src/main.rs create mode 100644 tools/unpacker/Cargo.toml create mode 100644 tools/unpacker/README.md create mode 100644 tools/unpacker/src/main.rs delete mode 100644 unpacker/Cargo.toml delete mode 100644 unpacker/README.md delete mode 100644 unpacker/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index d41f17b..24a0bfc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -843,15 +843,6 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb37767f6569cd834a413442455e0f066d0d522de8630436e2a1761d9726ba56" -[[package]] -name = "packer" -version = "0.1.0" -dependencies = [ - "byteorder", - "serde", - "serde_json", -] - [[package]] name = "paste" version = "1.0.15" diff --git a/Cargo.toml b/Cargo.toml index dce8213..d7bb610 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,6 @@ [workspace] resolver = "2" -members = [ - "libnres", - "nres-cli", - "packer", - "texture-decoder", - "unpacker", -] +members = ["libs/*", "tools/*"] [profile.release] codegen-units = 1 diff --git a/libnres/Cargo.toml b/libnres/Cargo.toml deleted file mode 100644 index 99539e7..0000000 --- a/libnres/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "libnres" -version = "0.1.4" -description = "Library for NRes files" -authors = ["Valentin Popov "] -homepage = "https://git.popov.link/valentineus/fparkan" -repository = "https://git.popov.link/valentineus/fparkan.git" -license = "GPL-2.0" -edition = "2021" -keywords = ["gamedev", "library", "nres"] - -[dependencies] -byteorder = "1.4" -log = "0.4" -miette = "7.0" -thiserror = "2.0" diff --git a/libnres/README.md b/libnres/README.md deleted file mode 100644 index 065bd40..0000000 --- a/libnres/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# Library for NRes files (Deprecated) - -Library for viewing and retrieving game resources of the game **"Parkan: Iron Strategy"**. -All versions of the game are supported: Demo, IS, IS: Part 1, IS: Part 2. -Supports files with `lib`, `trf`, `rlb` extensions. - -The files `gamefont.rlb` and `sprites.lib` are not supported. -This files have an unknown signature. - -## Example - -Example of extracting game resources: - -```rust -fn main() { - let file = std::fs::File::open("./voices.lib").unwrap(); - // Extracting the list of files - let list = libnres::reader::get_list(&file).unwrap(); - - for element in list { - // Extracting the contents of the file - let data = libnres::reader::get_file(&file, &element).unwrap(); - } -} -``` diff --git a/libnres/src/converter.rs b/libnres/src/converter.rs deleted file mode 100644 index bbf0535..0000000 --- a/libnres/src/converter.rs +++ /dev/null @@ -1,33 +0,0 @@ -use crate::error::ConverterError; - -/// Method for converting u32 to u64. -pub fn u32_to_u64(value: u32) -> Result { - match u64::try_from(value) { - Err(error) => Err(ConverterError::Infallible(error)), - Ok(result) => Ok(result), - } -} - -/// Method for converting u32 to usize. -pub fn u32_to_usize(value: u32) -> Result { - match usize::try_from(value) { - Err(error) => Err(ConverterError::TryFromIntError(error)), - Ok(result) => Ok(result), - } -} - -/// Method for converting u64 to u32. -pub fn u64_to_u32(value: u64) -> Result { - match u32::try_from(value) { - Err(error) => Err(ConverterError::TryFromIntError(error)), - Ok(result) => Ok(result), - } -} - -/// Method for converting usize to u32. -pub fn usize_to_u32(value: usize) -> Result { - match u32::try_from(value) { - Err(error) => Err(ConverterError::TryFromIntError(error)), - Ok(result) => Ok(result), - } -} diff --git a/libnres/src/error.rs b/libnres/src/error.rs deleted file mode 100644 index 440ab06..0000000 --- a/libnres/src/error.rs +++ /dev/null @@ -1,45 +0,0 @@ -extern crate miette; -extern crate thiserror; - -use miette::Diagnostic; -use thiserror::Error; - -#[derive(Error, Diagnostic, Debug)] -pub enum ConverterError { - #[error("error converting an value")] - #[diagnostic(code(libnres::infallible))] - Infallible(#[from] std::convert::Infallible), - - #[error("error converting an value")] - #[diagnostic(code(libnres::try_from_int_error))] - TryFromIntError(#[from] std::num::TryFromIntError), -} - -#[derive(Error, Diagnostic, Debug)] -pub enum ReaderError { - #[error(transparent)] - #[diagnostic(code(libnres::convert_error))] - ConvertValue(#[from] ConverterError), - - #[error("incorrect header format")] - #[diagnostic(code(libnres::list_type_error))] - IncorrectHeader, - - #[error("incorrect file size (expected {expected:?} bytes, received {received:?} bytes)")] - #[diagnostic(code(libnres::file_size_error))] - IncorrectSizeFile { expected: u32, received: u32 }, - - #[error( - "incorrect size of the file list (not a multiple of {expected:?}, received {received:?})" - )] - #[diagnostic(code(libnres::list_size_error))] - IncorrectSizeList { expected: u32, received: u32 }, - - #[error("resource file reading error")] - #[diagnostic(code(libnres::io_error))] - ReadFile(#[from] std::io::Error), - - #[error("file is too small (must be at least {expected:?} bytes, received {received:?} byte)")] - #[diagnostic(code(libnres::file_size_error))] - SmallFile { expected: u32, received: u32 }, -} diff --git a/libnres/src/lib.rs b/libnres/src/lib.rs deleted file mode 100644 index 40c0b32..0000000 --- a/libnres/src/lib.rs +++ /dev/null @@ -1,24 +0,0 @@ -/// First constant value of the NRes file ("NRes" characters in numeric) -pub const FILE_TYPE_1: u32 = 1936020046; -/// Second constant value of the NRes file -pub const FILE_TYPE_2: u32 = 256; -/// Size of the element item (in bytes) -pub const LIST_ELEMENT_SIZE: u32 = 64; -/// Minimum allowed file size (in bytes) -pub const MINIMUM_FILE_SIZE: u32 = 16; - -static DEBUG: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); - -mod converter; -mod error; -pub mod reader; - -/// Get debug status value -pub fn get_debug() -> bool { - DEBUG.load(std::sync::atomic::Ordering::Relaxed) -} - -/// Change debug status value -pub fn set_debug(value: bool) { - DEBUG.store(value, std::sync::atomic::Ordering::Relaxed) -} diff --git a/libnres/src/reader.rs b/libnres/src/reader.rs deleted file mode 100644 index 2a450ee..0000000 --- a/libnres/src/reader.rs +++ /dev/null @@ -1,227 +0,0 @@ -use std::io::{Read, Seek}; - -use byteorder::ByteOrder; - -use crate::error::ReaderError; -use crate::{converter, FILE_TYPE_1, FILE_TYPE_2, LIST_ELEMENT_SIZE, MINIMUM_FILE_SIZE}; - -#[derive(Debug)] -pub struct ListElement { - /// Unknown parameter - _unknown0: i32, - /// Unknown parameter - _unknown1: i32, - /// Unknown parameter - _unknown2: i32, - /// File extension - pub extension: String, - /// Identifier or sequence number - pub index: u32, - /// File name - pub name: String, - /// Position in the file - pub position: u32, - /// File size (in bytes) - pub size: u32, -} - -impl ListElement { - /// Get full name of the file - pub fn get_filename(&self) -> String { - format!("{}.{}", self.name, self.extension) - } -} - -#[derive(Debug)] -pub struct FileHeader { - /// File size - size: u32, - /// Number of files - total: u32, - /// First constant value - type1: u32, - /// Second constant value - type2: u32, -} - -/// Get a packed file data -pub fn get_file(file: &std::fs::File, element: &ListElement) -> Result, ReaderError> { - let size = get_file_size(file)?; - check_file_size(size)?; - - let header = get_file_header(file)?; - check_file_header(&header, size)?; - - let data = get_element_data(file, element)?; - Ok(data) -} - -/// Get a list of packed files -pub fn get_list(file: &std::fs::File) -> Result, ReaderError> { - let mut list: Vec = Vec::new(); - - let size = get_file_size(file)?; - check_file_size(size)?; - - let header = get_file_header(file)?; - check_file_header(&header, size)?; - - get_file_list(file, &header, &mut list)?; - - Ok(list) -} - -fn check_file_header(header: &FileHeader, size: u32) -> Result<(), ReaderError> { - if header.type1 != FILE_TYPE_1 || header.type2 != FILE_TYPE_2 { - return Err(ReaderError::IncorrectHeader); - } - - if header.size != size { - return Err(ReaderError::IncorrectSizeFile { - expected: size, - received: header.size, - }); - } - - Ok(()) -} - -fn check_file_size(size: u32) -> Result<(), ReaderError> { - if size < MINIMUM_FILE_SIZE { - return Err(ReaderError::SmallFile { - expected: MINIMUM_FILE_SIZE, - received: size, - }); - } - - Ok(()) -} - -fn get_element_data(file: &std::fs::File, element: &ListElement) -> Result, ReaderError> { - let position = converter::u32_to_u64(element.position)?; - let size = converter::u32_to_usize(element.size)?; - - let mut reader = std::io::BufReader::new(file); - let mut buffer = vec![0u8; size]; - - if let Err(error) = reader.seek(std::io::SeekFrom::Start(position)) { - return Err(ReaderError::ReadFile(error)); - }; - - if let Err(error) = reader.read_exact(&mut buffer) { - return Err(ReaderError::ReadFile(error)); - }; - - Ok(buffer) -} - -fn get_element_position(index: u32) -> Result<(usize, usize), ReaderError> { - let from = converter::u32_to_usize(index * LIST_ELEMENT_SIZE)?; - let to = converter::u32_to_usize((index * LIST_ELEMENT_SIZE) + LIST_ELEMENT_SIZE)?; - Ok((from, to)) -} - -fn get_file_header(file: &std::fs::File) -> Result { - let mut reader = std::io::BufReader::new(file); - let mut buffer = vec![0u8; MINIMUM_FILE_SIZE as usize]; - - if let Err(error) = reader.seek(std::io::SeekFrom::Start(0)) { - return Err(ReaderError::ReadFile(error)); - }; - - if let Err(error) = reader.read_exact(&mut buffer) { - return Err(ReaderError::ReadFile(error)); - }; - - let header = FileHeader { - size: byteorder::LittleEndian::read_u32(&buffer[12..16]), - total: byteorder::LittleEndian::read_u32(&buffer[8..12]), - type1: byteorder::LittleEndian::read_u32(&buffer[0..4]), - type2: byteorder::LittleEndian::read_u32(&buffer[4..8]), - }; - - buffer.clear(); - Ok(header) -} - -fn get_file_list( - file: &std::fs::File, - header: &FileHeader, - list: &mut Vec, -) -> Result<(), ReaderError> { - let (start_position, list_size) = get_list_position(header)?; - let mut reader = std::io::BufReader::new(file); - let mut buffer = vec![0u8; list_size]; - - if let Err(error) = reader.seek(std::io::SeekFrom::Start(start_position)) { - return Err(ReaderError::ReadFile(error)); - }; - - if let Err(error) = reader.read_exact(&mut buffer) { - return Err(ReaderError::ReadFile(error)); - } - - let buffer_size = converter::usize_to_u32(buffer.len())?; - - if buffer_size % LIST_ELEMENT_SIZE != 0 { - return Err(ReaderError::IncorrectSizeList { - expected: LIST_ELEMENT_SIZE, - received: buffer_size, - }); - } - - for i in 0..(buffer_size / LIST_ELEMENT_SIZE) { - let (from, to) = get_element_position(i)?; - let chunk: &[u8] = &buffer[from..to]; - - let element = get_list_element(chunk)?; - list.push(element); - } - - buffer.clear(); - Ok(()) -} - -fn get_file_size(file: &std::fs::File) -> Result { - let metadata = match file.metadata() { - Err(error) => return Err(ReaderError::ReadFile(error)), - Ok(value) => value, - }; - - let result = converter::u64_to_u32(metadata.len())?; - Ok(result) -} - -fn get_list_element(buffer: &[u8]) -> Result { - let index = byteorder::LittleEndian::read_u32(&buffer[60..64]); - let position = byteorder::LittleEndian::read_u32(&buffer[56..60]); - let size = byteorder::LittleEndian::read_u32(&buffer[12..16]); - let unknown0 = byteorder::LittleEndian::read_i32(&buffer[4..8]); - let unknown1 = byteorder::LittleEndian::read_i32(&buffer[8..12]); - let unknown2 = byteorder::LittleEndian::read_i32(&buffer[16..20]); - - let extension = String::from_utf8_lossy(&buffer[0..4]) - .trim_matches(char::from(0)) - .to_string(); - - let name = String::from_utf8_lossy(&buffer[20..56]) - .trim_matches(char::from(0)) - .to_string(); - - Ok(ListElement { - _unknown0: unknown0, - _unknown1: unknown1, - _unknown2: unknown2, - extension, - index, - name, - position, - size, - }) -} - -fn get_list_position(header: &FileHeader) -> Result<(u64, usize), ReaderError> { - let position = converter::u32_to_u64(header.size - (header.total * LIST_ELEMENT_SIZE))?; - let size = converter::u32_to_usize(header.total * LIST_ELEMENT_SIZE)?; - Ok((position, size)) -} diff --git a/libs/nres/Cargo.toml b/libs/nres/Cargo.toml new file mode 100644 index 0000000..804f5c0 --- /dev/null +++ b/libs/nres/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "libnres" +version = "0.1.4" +edition = "2021" + +[dependencies] +byteorder = "1.4" +log = "0.4" +miette = "7.0" +thiserror = "2.0" diff --git a/libs/nres/src/converter.rs b/libs/nres/src/converter.rs new file mode 100644 index 0000000..bbf0535 --- /dev/null +++ b/libs/nres/src/converter.rs @@ -0,0 +1,33 @@ +use crate::error::ConverterError; + +/// Method for converting u32 to u64. +pub fn u32_to_u64(value: u32) -> Result { + match u64::try_from(value) { + Err(error) => Err(ConverterError::Infallible(error)), + Ok(result) => Ok(result), + } +} + +/// Method for converting u32 to usize. +pub fn u32_to_usize(value: u32) -> Result { + match usize::try_from(value) { + Err(error) => Err(ConverterError::TryFromIntError(error)), + Ok(result) => Ok(result), + } +} + +/// Method for converting u64 to u32. +pub fn u64_to_u32(value: u64) -> Result { + match u32::try_from(value) { + Err(error) => Err(ConverterError::TryFromIntError(error)), + Ok(result) => Ok(result), + } +} + +/// Method for converting usize to u32. +pub fn usize_to_u32(value: usize) -> Result { + match u32::try_from(value) { + Err(error) => Err(ConverterError::TryFromIntError(error)), + Ok(result) => Ok(result), + } +} diff --git a/libs/nres/src/error.rs b/libs/nres/src/error.rs new file mode 100644 index 0000000..440ab06 --- /dev/null +++ b/libs/nres/src/error.rs @@ -0,0 +1,45 @@ +extern crate miette; +extern crate thiserror; + +use miette::Diagnostic; +use thiserror::Error; + +#[derive(Error, Diagnostic, Debug)] +pub enum ConverterError { + #[error("error converting an value")] + #[diagnostic(code(libnres::infallible))] + Infallible(#[from] std::convert::Infallible), + + #[error("error converting an value")] + #[diagnostic(code(libnres::try_from_int_error))] + TryFromIntError(#[from] std::num::TryFromIntError), +} + +#[derive(Error, Diagnostic, Debug)] +pub enum ReaderError { + #[error(transparent)] + #[diagnostic(code(libnres::convert_error))] + ConvertValue(#[from] ConverterError), + + #[error("incorrect header format")] + #[diagnostic(code(libnres::list_type_error))] + IncorrectHeader, + + #[error("incorrect file size (expected {expected:?} bytes, received {received:?} bytes)")] + #[diagnostic(code(libnres::file_size_error))] + IncorrectSizeFile { expected: u32, received: u32 }, + + #[error( + "incorrect size of the file list (not a multiple of {expected:?}, received {received:?})" + )] + #[diagnostic(code(libnres::list_size_error))] + IncorrectSizeList { expected: u32, received: u32 }, + + #[error("resource file reading error")] + #[diagnostic(code(libnres::io_error))] + ReadFile(#[from] std::io::Error), + + #[error("file is too small (must be at least {expected:?} bytes, received {received:?} byte)")] + #[diagnostic(code(libnres::file_size_error))] + SmallFile { expected: u32, received: u32 }, +} diff --git a/libs/nres/src/lib.rs b/libs/nres/src/lib.rs new file mode 100644 index 0000000..40c0b32 --- /dev/null +++ b/libs/nres/src/lib.rs @@ -0,0 +1,24 @@ +/// First constant value of the NRes file ("NRes" characters in numeric) +pub const FILE_TYPE_1: u32 = 1936020046; +/// Second constant value of the NRes file +pub const FILE_TYPE_2: u32 = 256; +/// Size of the element item (in bytes) +pub const LIST_ELEMENT_SIZE: u32 = 64; +/// Minimum allowed file size (in bytes) +pub const MINIMUM_FILE_SIZE: u32 = 16; + +static DEBUG: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); + +mod converter; +mod error; +pub mod reader; + +/// Get debug status value +pub fn get_debug() -> bool { + DEBUG.load(std::sync::atomic::Ordering::Relaxed) +} + +/// Change debug status value +pub fn set_debug(value: bool) { + DEBUG.store(value, std::sync::atomic::Ordering::Relaxed) +} diff --git a/libs/nres/src/reader.rs b/libs/nres/src/reader.rs new file mode 100644 index 0000000..2a450ee --- /dev/null +++ b/libs/nres/src/reader.rs @@ -0,0 +1,227 @@ +use std::io::{Read, Seek}; + +use byteorder::ByteOrder; + +use crate::error::ReaderError; +use crate::{converter, FILE_TYPE_1, FILE_TYPE_2, LIST_ELEMENT_SIZE, MINIMUM_FILE_SIZE}; + +#[derive(Debug)] +pub struct ListElement { + /// Unknown parameter + _unknown0: i32, + /// Unknown parameter + _unknown1: i32, + /// Unknown parameter + _unknown2: i32, + /// File extension + pub extension: String, + /// Identifier or sequence number + pub index: u32, + /// File name + pub name: String, + /// Position in the file + pub position: u32, + /// File size (in bytes) + pub size: u32, +} + +impl ListElement { + /// Get full name of the file + pub fn get_filename(&self) -> String { + format!("{}.{}", self.name, self.extension) + } +} + +#[derive(Debug)] +pub struct FileHeader { + /// File size + size: u32, + /// Number of files + total: u32, + /// First constant value + type1: u32, + /// Second constant value + type2: u32, +} + +/// Get a packed file data +pub fn get_file(file: &std::fs::File, element: &ListElement) -> Result, ReaderError> { + let size = get_file_size(file)?; + check_file_size(size)?; + + let header = get_file_header(file)?; + check_file_header(&header, size)?; + + let data = get_element_data(file, element)?; + Ok(data) +} + +/// Get a list of packed files +pub fn get_list(file: &std::fs::File) -> Result, ReaderError> { + let mut list: Vec = Vec::new(); + + let size = get_file_size(file)?; + check_file_size(size)?; + + let header = get_file_header(file)?; + check_file_header(&header, size)?; + + get_file_list(file, &header, &mut list)?; + + Ok(list) +} + +fn check_file_header(header: &FileHeader, size: u32) -> Result<(), ReaderError> { + if header.type1 != FILE_TYPE_1 || header.type2 != FILE_TYPE_2 { + return Err(ReaderError::IncorrectHeader); + } + + if header.size != size { + return Err(ReaderError::IncorrectSizeFile { + expected: size, + received: header.size, + }); + } + + Ok(()) +} + +fn check_file_size(size: u32) -> Result<(), ReaderError> { + if size < MINIMUM_FILE_SIZE { + return Err(ReaderError::SmallFile { + expected: MINIMUM_FILE_SIZE, + received: size, + }); + } + + Ok(()) +} + +fn get_element_data(file: &std::fs::File, element: &ListElement) -> Result, ReaderError> { + let position = converter::u32_to_u64(element.position)?; + let size = converter::u32_to_usize(element.size)?; + + let mut reader = std::io::BufReader::new(file); + let mut buffer = vec![0u8; size]; + + if let Err(error) = reader.seek(std::io::SeekFrom::Start(position)) { + return Err(ReaderError::ReadFile(error)); + }; + + if let Err(error) = reader.read_exact(&mut buffer) { + return Err(ReaderError::ReadFile(error)); + }; + + Ok(buffer) +} + +fn get_element_position(index: u32) -> Result<(usize, usize), ReaderError> { + let from = converter::u32_to_usize(index * LIST_ELEMENT_SIZE)?; + let to = converter::u32_to_usize((index * LIST_ELEMENT_SIZE) + LIST_ELEMENT_SIZE)?; + Ok((from, to)) +} + +fn get_file_header(file: &std::fs::File) -> Result { + let mut reader = std::io::BufReader::new(file); + let mut buffer = vec![0u8; MINIMUM_FILE_SIZE as usize]; + + if let Err(error) = reader.seek(std::io::SeekFrom::Start(0)) { + return Err(ReaderError::ReadFile(error)); + }; + + if let Err(error) = reader.read_exact(&mut buffer) { + return Err(ReaderError::ReadFile(error)); + }; + + let header = FileHeader { + size: byteorder::LittleEndian::read_u32(&buffer[12..16]), + total: byteorder::LittleEndian::read_u32(&buffer[8..12]), + type1: byteorder::LittleEndian::read_u32(&buffer[0..4]), + type2: byteorder::LittleEndian::read_u32(&buffer[4..8]), + }; + + buffer.clear(); + Ok(header) +} + +fn get_file_list( + file: &std::fs::File, + header: &FileHeader, + list: &mut Vec, +) -> Result<(), ReaderError> { + let (start_position, list_size) = get_list_position(header)?; + let mut reader = std::io::BufReader::new(file); + let mut buffer = vec![0u8; list_size]; + + if let Err(error) = reader.seek(std::io::SeekFrom::Start(start_position)) { + return Err(ReaderError::ReadFile(error)); + }; + + if let Err(error) = reader.read_exact(&mut buffer) { + return Err(ReaderError::ReadFile(error)); + } + + let buffer_size = converter::usize_to_u32(buffer.len())?; + + if buffer_size % LIST_ELEMENT_SIZE != 0 { + return Err(ReaderError::IncorrectSizeList { + expected: LIST_ELEMENT_SIZE, + received: buffer_size, + }); + } + + for i in 0..(buffer_size / LIST_ELEMENT_SIZE) { + let (from, to) = get_element_position(i)?; + let chunk: &[u8] = &buffer[from..to]; + + let element = get_list_element(chunk)?; + list.push(element); + } + + buffer.clear(); + Ok(()) +} + +fn get_file_size(file: &std::fs::File) -> Result { + let metadata = match file.metadata() { + Err(error) => return Err(ReaderError::ReadFile(error)), + Ok(value) => value, + }; + + let result = converter::u64_to_u32(metadata.len())?; + Ok(result) +} + +fn get_list_element(buffer: &[u8]) -> Result { + let index = byteorder::LittleEndian::read_u32(&buffer[60..64]); + let position = byteorder::LittleEndian::read_u32(&buffer[56..60]); + let size = byteorder::LittleEndian::read_u32(&buffer[12..16]); + let unknown0 = byteorder::LittleEndian::read_i32(&buffer[4..8]); + let unknown1 = byteorder::LittleEndian::read_i32(&buffer[8..12]); + let unknown2 = byteorder::LittleEndian::read_i32(&buffer[16..20]); + + let extension = String::from_utf8_lossy(&buffer[0..4]) + .trim_matches(char::from(0)) + .to_string(); + + let name = String::from_utf8_lossy(&buffer[20..56]) + .trim_matches(char::from(0)) + .to_string(); + + Ok(ListElement { + _unknown0: unknown0, + _unknown1: unknown1, + _unknown2: unknown2, + extension, + index, + name, + position, + size, + }) +} + +fn get_list_position(header: &FileHeader) -> Result<(u64, usize), ReaderError> { + let position = converter::u32_to_u64(header.size - (header.total * LIST_ELEMENT_SIZE))?; + let size = converter::u32_to_usize(header.total * LIST_ELEMENT_SIZE)?; + Ok((position, size)) +} diff --git a/nres-cli/Cargo.toml b/nres-cli/Cargo.toml deleted file mode 100644 index a6d863c..0000000 --- a/nres-cli/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "nres-cli" -version = "0.2.3" -description = "Console tool for NRes files" -authors = ["Valentin Popov "] -homepage = "https://git.popov.link/valentineus/fparkan" -repository = "https://git.popov.link/valentineus/fparkan.git" -license = "GPL-2.0" -edition = "2021" -keywords = ["cli", "gamedev", "nres"] - -[dependencies] -byteorder = "1.4" -clap = { version = "4.2", features = ["derive"] } -console = "0.15" -dialoguer = { version = "0.11", features = ["completion"] } -indicatif = "0.17" -libnres = { version = "0.1", path = "../libnres" } -miette = { version = "7.0", features = ["fancy"] } -tempdir = "0.3" diff --git a/nres-cli/README.md b/nres-cli/README.md deleted file mode 100644 index 65a6602..0000000 --- a/nres-cli/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# Console tool for NRes files (Deprecated) - -## Commands - -- `extract` - Extract game resources from a "NRes" file. -- `ls` - Get a list of files in a "NRes" file. diff --git a/nres-cli/src/main.rs b/nres-cli/src/main.rs deleted file mode 100644 index 85086cb..0000000 --- a/nres-cli/src/main.rs +++ /dev/null @@ -1,198 +0,0 @@ -extern crate core; -extern crate libnres; - -use std::io::Write; - -use clap::{Parser, Subcommand}; -use miette::{IntoDiagnostic, Result}; - -#[derive(Parser, Debug)] -#[command(name = "NRes CLI")] -#[command(about, author, version, long_about = None)] -struct Cli { - #[command(subcommand)] - command: Commands, -} - -#[derive(Subcommand, Debug)] -enum Commands { - /// Check if the "NRes" file can be extract - Check { - /// "NRes" file - file: String, - }, - /// Print debugging information on the "NRes" file - #[command(arg_required_else_help = true)] - Debug { - /// "NRes" file - file: String, - /// Filter results by file name - #[arg(long)] - name: Option, - }, - /// Extract files or a file from the "NRes" file - #[command(arg_required_else_help = true)] - Extract { - /// "NRes" file - file: String, - /// Overwrite files - #[arg(short, long, default_value_t = false, value_name = "TRUE|FALSE")] - force: bool, - /// Outbound directory - #[arg(short, long, value_name = "DIR")] - out: String, - }, - /// Print a list of files in the "NRes" file - #[command(arg_required_else_help = true)] - Ls { - /// "NRes" file - file: String, - }, -} - -pub fn main() -> Result<()> { - let stdout = console::Term::stdout(); - let cli = Cli::parse(); - - match cli.command { - Commands::Check { file } => command_check(stdout, file)?, - Commands::Debug { file, name } => command_debug(stdout, file, name)?, - Commands::Extract { file, force, out } => command_extract(stdout, file, out, force)?, - Commands::Ls { file } => command_ls(stdout, file)?, - } - - Ok(()) -} - -fn command_check(_stdout: console::Term, file: String) -> Result<()> { - let file = std::fs::File::open(file).into_diagnostic()?; - let list = libnres::reader::get_list(&file).into_diagnostic()?; - let tmp = tempdir::TempDir::new("nres").into_diagnostic()?; - let bar = indicatif::ProgressBar::new(list.len() as u64); - - bar.set_style(get_bar_style()?); - - for element in list { - bar.set_message(element.get_filename()); - - let path = tmp.path().join(element.get_filename()); - let mut output = std::fs::File::create(path).into_diagnostic()?; - let mut buffer = libnres::reader::get_file(&file, &element).into_diagnostic()?; - - output.write_all(&buffer).into_diagnostic()?; - buffer.clear(); - bar.inc(1); - } - - bar.finish(); - - Ok(()) -} - -fn command_debug(stdout: console::Term, file: String, name: Option) -> Result<()> { - let file = std::fs::File::open(file).into_diagnostic()?; - let mut list = libnres::reader::get_list(&file).into_diagnostic()?; - - let mut total_files_size: u32 = 0; - let mut total_files_gap: u32 = 0; - let mut total_files: u32 = 0; - - for (index, item) in list.iter().enumerate() { - total_files_size += item.size; - total_files += 1; - let mut gap = 0; - - if index > 1 { - let previous_item = &list[index - 1]; - gap = item.position - (previous_item.position + previous_item.size); - } - - total_files_gap += gap; - } - - if let Some(name) = name { - list.retain(|item| item.name.contains(&name)); - }; - - for (index, item) in list.iter().enumerate() { - let mut gap = 0; - - if index > 1 { - let previous_item = &list[index - 1]; - gap = item.position - (previous_item.position + previous_item.size); - } - - let text = format!("Index: {};\nGap: {};\nItem: {:#?};\n", index, gap, item); - stdout.write_line(&text).into_diagnostic()?; - } - - let text = format!( - "Total files: {};\nTotal files gap: {} (bytes);\nTotal files size: {} (bytes);", - total_files, total_files_gap, total_files_size - ); - - stdout.write_line(&text).into_diagnostic()?; - - Ok(()) -} - -fn command_extract(_stdout: console::Term, file: String, out: String, force: bool) -> Result<()> { - let file = std::fs::File::open(file).into_diagnostic()?; - let list = libnres::reader::get_list(&file).into_diagnostic()?; - let bar = indicatif::ProgressBar::new(list.len() as u64); - - bar.set_style(get_bar_style()?); - - for element in list { - bar.set_message(element.get_filename()); - - let path = format!("{}/{}", out, element.get_filename()); - - if !force && is_exist_file(&path) { - let message = format!("File \"{}\" exists. Overwrite it?", path); - - if !dialoguer::Confirm::new() - .with_prompt(message) - .interact() - .into_diagnostic()? - { - continue; - } - } - - let mut output = std::fs::File::create(path).into_diagnostic()?; - let mut buffer = libnres::reader::get_file(&file, &element).into_diagnostic()?; - - output.write_all(&buffer).into_diagnostic()?; - buffer.clear(); - bar.inc(1); - } - - bar.finish(); - - Ok(()) -} - -fn command_ls(stdout: console::Term, file: String) -> Result<()> { - let file = std::fs::File::open(file).into_diagnostic()?; - let list = libnres::reader::get_list(&file).into_diagnostic()?; - - for element in list { - stdout.write_line(&element.name).into_diagnostic()?; - } - - Ok(()) -} - -fn get_bar_style() -> Result { - Ok( - indicatif::ProgressStyle::with_template("[{bar:32}] {pos:>7}/{len:7} {msg}") - .into_diagnostic()? - .progress_chars("=>-"), - ) -} - -fn is_exist_file(path: &String) -> bool { - let metadata = std::path::Path::new(path); - metadata.exists() -} diff --git a/texture-decoder/Cargo.toml b/texture-decoder/Cargo.toml deleted file mode 100644 index 0d11da6..0000000 --- a/texture-decoder/Cargo.toml +++ /dev/null @@ -1,8 +0,0 @@ -[package] -name = "texture-decoder" -version = "0.1.0" -edition = "2021" - -[dependencies] -byteorder = "1.4.3" -image = "0.25.0" diff --git a/texture-decoder/README.md b/texture-decoder/README.md deleted file mode 100644 index 8fca059..0000000 --- a/texture-decoder/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Декодировщик текстур - -Сборка: - -```bash -cargo build --release -``` - -Запуск: - -```bash -./target/release/texture-decoder ./out/AIM_02.0 ./out/AIM_02.0.png -``` \ No newline at end of file diff --git a/texture-decoder/src/main.rs b/texture-decoder/src/main.rs deleted file mode 100644 index 26c7edd..0000000 --- a/texture-decoder/src/main.rs +++ /dev/null @@ -1,41 +0,0 @@ -use std::io::Read; - -use byteorder::ReadBytesExt; -use image::Rgba; - -fn decode_texture(file_path: &str, output_path: &str) -> Result<(), std::io::Error> { - // Читаем файл - let mut file = std::fs::File::open(file_path)?; - let mut buffer: Vec = Vec::new(); - file.read_to_end(&mut buffer)?; - - // Декодируем метаданные - let mut cursor = std::io::Cursor::new(&buffer[4..]); - let img_width = cursor.read_u32::()?; - let img_height = cursor.read_u32::()?; - - // Пропустить оставшиеся байты метаданных - cursor.set_position(20); - - // Извлекаем данные изображения - let image_data = buffer[cursor.position() as usize..].to_vec(); - let img = - image::ImageBuffer::, _>::from_raw(img_width, img_height, image_data.to_vec()) - .expect("Failed to decode image"); - - // Сохраняем изображение - img.save(output_path).unwrap(); - - Ok(()) -} - -fn main() { - let args: Vec = std::env::args().collect(); - - let input = &args[1]; - let output = &args[2]; - - if let Err(err) = decode_texture(input, output) { - eprintln!("Error: {}", err) - } -} diff --git a/tools/nres-cli/Cargo.toml b/tools/nres-cli/Cargo.toml new file mode 100644 index 0000000..dd0ced6 --- /dev/null +++ b/tools/nres-cli/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "nres-cli" +version = "0.2.3" +edition = "2021" + +[dependencies] +byteorder = "1.4" +clap = { version = "4.2", features = ["derive"] } +console = "0.15" +dialoguer = { version = "0.11", features = ["completion"] } +indicatif = "0.17" +libnres = { version = "0.1", path = "../../libs/nres" } +miette = { version = "7.0", features = ["fancy"] } +tempdir = "0.3" diff --git a/tools/nres-cli/README.md b/tools/nres-cli/README.md new file mode 100644 index 0000000..fee1420 --- /dev/null +++ b/tools/nres-cli/README.md @@ -0,0 +1,6 @@ +# Console tool for NRes files (Deprecated) + +## Commands + +- `extract` - Extract game resources from a "NRes" file. +- `ls` - Get a list of files in a "NRes" file. \ No newline at end of file diff --git a/tools/nres-cli/src/main.rs b/tools/nres-cli/src/main.rs new file mode 100644 index 0000000..85086cb --- /dev/null +++ b/tools/nres-cli/src/main.rs @@ -0,0 +1,198 @@ +extern crate core; +extern crate libnres; + +use std::io::Write; + +use clap::{Parser, Subcommand}; +use miette::{IntoDiagnostic, Result}; + +#[derive(Parser, Debug)] +#[command(name = "NRes CLI")] +#[command(about, author, version, long_about = None)] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand, Debug)] +enum Commands { + /// Check if the "NRes" file can be extract + Check { + /// "NRes" file + file: String, + }, + /// Print debugging information on the "NRes" file + #[command(arg_required_else_help = true)] + Debug { + /// "NRes" file + file: String, + /// Filter results by file name + #[arg(long)] + name: Option, + }, + /// Extract files or a file from the "NRes" file + #[command(arg_required_else_help = true)] + Extract { + /// "NRes" file + file: String, + /// Overwrite files + #[arg(short, long, default_value_t = false, value_name = "TRUE|FALSE")] + force: bool, + /// Outbound directory + #[arg(short, long, value_name = "DIR")] + out: String, + }, + /// Print a list of files in the "NRes" file + #[command(arg_required_else_help = true)] + Ls { + /// "NRes" file + file: String, + }, +} + +pub fn main() -> Result<()> { + let stdout = console::Term::stdout(); + let cli = Cli::parse(); + + match cli.command { + Commands::Check { file } => command_check(stdout, file)?, + Commands::Debug { file, name } => command_debug(stdout, file, name)?, + Commands::Extract { file, force, out } => command_extract(stdout, file, out, force)?, + Commands::Ls { file } => command_ls(stdout, file)?, + } + + Ok(()) +} + +fn command_check(_stdout: console::Term, file: String) -> Result<()> { + let file = std::fs::File::open(file).into_diagnostic()?; + let list = libnres::reader::get_list(&file).into_diagnostic()?; + let tmp = tempdir::TempDir::new("nres").into_diagnostic()?; + let bar = indicatif::ProgressBar::new(list.len() as u64); + + bar.set_style(get_bar_style()?); + + for element in list { + bar.set_message(element.get_filename()); + + let path = tmp.path().join(element.get_filename()); + let mut output = std::fs::File::create(path).into_diagnostic()?; + let mut buffer = libnres::reader::get_file(&file, &element).into_diagnostic()?; + + output.write_all(&buffer).into_diagnostic()?; + buffer.clear(); + bar.inc(1); + } + + bar.finish(); + + Ok(()) +} + +fn command_debug(stdout: console::Term, file: String, name: Option) -> Result<()> { + let file = std::fs::File::open(file).into_diagnostic()?; + let mut list = libnres::reader::get_list(&file).into_diagnostic()?; + + let mut total_files_size: u32 = 0; + let mut total_files_gap: u32 = 0; + let mut total_files: u32 = 0; + + for (index, item) in list.iter().enumerate() { + total_files_size += item.size; + total_files += 1; + let mut gap = 0; + + if index > 1 { + let previous_item = &list[index - 1]; + gap = item.position - (previous_item.position + previous_item.size); + } + + total_files_gap += gap; + } + + if let Some(name) = name { + list.retain(|item| item.name.contains(&name)); + }; + + for (index, item) in list.iter().enumerate() { + let mut gap = 0; + + if index > 1 { + let previous_item = &list[index - 1]; + gap = item.position - (previous_item.position + previous_item.size); + } + + let text = format!("Index: {};\nGap: {};\nItem: {:#?};\n", index, gap, item); + stdout.write_line(&text).into_diagnostic()?; + } + + let text = format!( + "Total files: {};\nTotal files gap: {} (bytes);\nTotal files size: {} (bytes);", + total_files, total_files_gap, total_files_size + ); + + stdout.write_line(&text).into_diagnostic()?; + + Ok(()) +} + +fn command_extract(_stdout: console::Term, file: String, out: String, force: bool) -> Result<()> { + let file = std::fs::File::open(file).into_diagnostic()?; + let list = libnres::reader::get_list(&file).into_diagnostic()?; + let bar = indicatif::ProgressBar::new(list.len() as u64); + + bar.set_style(get_bar_style()?); + + for element in list { + bar.set_message(element.get_filename()); + + let path = format!("{}/{}", out, element.get_filename()); + + if !force && is_exist_file(&path) { + let message = format!("File \"{}\" exists. Overwrite it?", path); + + if !dialoguer::Confirm::new() + .with_prompt(message) + .interact() + .into_diagnostic()? + { + continue; + } + } + + let mut output = std::fs::File::create(path).into_diagnostic()?; + let mut buffer = libnres::reader::get_file(&file, &element).into_diagnostic()?; + + output.write_all(&buffer).into_diagnostic()?; + buffer.clear(); + bar.inc(1); + } + + bar.finish(); + + Ok(()) +} + +fn command_ls(stdout: console::Term, file: String) -> Result<()> { + let file = std::fs::File::open(file).into_diagnostic()?; + let list = libnres::reader::get_list(&file).into_diagnostic()?; + + for element in list { + stdout.write_line(&element.name).into_diagnostic()?; + } + + Ok(()) +} + +fn get_bar_style() -> Result { + Ok( + indicatif::ProgressStyle::with_template("[{bar:32}] {pos:>7}/{len:7} {msg}") + .into_diagnostic()? + .progress_chars("=>-"), + ) +} + +fn is_exist_file(path: &String) -> bool { + let metadata = std::path::Path::new(path); + metadata.exists() +} diff --git a/tools/texture-decoder/Cargo.toml b/tools/texture-decoder/Cargo.toml new file mode 100644 index 0000000..0d11da6 --- /dev/null +++ b/tools/texture-decoder/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "texture-decoder" +version = "0.1.0" +edition = "2021" + +[dependencies] +byteorder = "1.4.3" +image = "0.25.0" diff --git a/tools/texture-decoder/README.md b/tools/texture-decoder/README.md new file mode 100644 index 0000000..8fca059 --- /dev/null +++ b/tools/texture-decoder/README.md @@ -0,0 +1,13 @@ +# Декодировщик текстур + +Сборка: + +```bash +cargo build --release +``` + +Запуск: + +```bash +./target/release/texture-decoder ./out/AIM_02.0 ./out/AIM_02.0.png +``` \ No newline at end of file diff --git a/tools/texture-decoder/src/main.rs b/tools/texture-decoder/src/main.rs new file mode 100644 index 0000000..26c7edd --- /dev/null +++ b/tools/texture-decoder/src/main.rs @@ -0,0 +1,41 @@ +use std::io::Read; + +use byteorder::ReadBytesExt; +use image::Rgba; + +fn decode_texture(file_path: &str, output_path: &str) -> Result<(), std::io::Error> { + // Читаем файл + let mut file = std::fs::File::open(file_path)?; + let mut buffer: Vec = Vec::new(); + file.read_to_end(&mut buffer)?; + + // Декодируем метаданные + let mut cursor = std::io::Cursor::new(&buffer[4..]); + let img_width = cursor.read_u32::()?; + let img_height = cursor.read_u32::()?; + + // Пропустить оставшиеся байты метаданных + cursor.set_position(20); + + // Извлекаем данные изображения + let image_data = buffer[cursor.position() as usize..].to_vec(); + let img = + image::ImageBuffer::, _>::from_raw(img_width, img_height, image_data.to_vec()) + .expect("Failed to decode image"); + + // Сохраняем изображение + img.save(output_path).unwrap(); + + Ok(()) +} + +fn main() { + let args: Vec = std::env::args().collect(); + + let input = &args[1]; + let output = &args[2]; + + if let Err(err) = decode_texture(input, output) { + eprintln!("Error: {}", err) + } +} diff --git a/tools/unpacker/Cargo.toml b/tools/unpacker/Cargo.toml new file mode 100644 index 0000000..adb64ec --- /dev/null +++ b/tools/unpacker/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "unpacker" +version = "0.1.1" +edition = "2021" + +[dependencies] +byteorder = "1.4.3" +serde = { version = "1.0.160", features = ["derive"] } +serde_json = "1.0.96" diff --git a/tools/unpacker/README.md b/tools/unpacker/README.md new file mode 100644 index 0000000..311e0eb --- /dev/null +++ b/tools/unpacker/README.md @@ -0,0 +1,41 @@ +# NRes Game Resource Unpacker + +At the moment, this is a demonstration of the NRes game resource unpacking algorithm in action. +It unpacks 100% of the NRes game resources for the game "Parkan: Iron Strategy". +The unpacked resources can be packed again using the [packer](../packer) utility and replace the original game files. + +__Attention!__ +This is a test version of the utility. +It overwrites existing files without asking. + +## Building + +To build the tools, you need to run the following command in the root directory: + +```bash +cargo build --release +``` + +## Running + +You can run the utility with the following command: + +```bash +./target/release/unpacker /path/to/file.ex /path/to/output +``` + +- `/path/to/file.ex`: This is the file containing the game resources that will be unpacked. +- `/path/to/output`: This is the directory where the unpacked files will be placed. + +## How it Works + +The structure describing the packed game resources is not fully understood yet. +Therefore, the utility saves unpacked files in the format `file_name.file_index` because some files have the same name. + +Additionally, an `index.json` file is created, which is important for re-packing the files. +This file lists all the fields that game resources have in their packed form. +It is essential to preserve the file index for the game to function correctly, as the game engine looks for the necessary files by index. + +Files can be replaced and packed back using the [packer](../packer). +The newly obtained game resource files are correctly processed by the game engine. +For example, sounds and 3D models of warbots' weapons were successfully replaced. \ No newline at end of file diff --git a/tools/unpacker/src/main.rs b/tools/unpacker/src/main.rs new file mode 100644 index 0000000..2a84688 --- /dev/null +++ b/tools/unpacker/src/main.rs @@ -0,0 +1,124 @@ +use std::env; +use std::fs::File; +use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write}; + +use byteorder::{ByteOrder, LittleEndian}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug)] +pub struct FileHeader { + pub size: u32, + pub total: u32, + pub type1: u32, + pub type2: u32, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct ListElement { + pub extension: String, + pub index: u32, + pub name: String, + #[serde(skip_serializing)] + pub position: u32, + #[serde(skip_serializing)] + pub size: u32, + pub unknown0: u32, + pub unknown1: u32, + pub unknown2: u32, +} + +fn main() { + let args: Vec = env::args().collect(); + + let input = &args[1]; + let output = &args[2]; + + unpack(String::from(input), String::from(output)); +} + +fn unpack(input: String, output: String) { + let file = File::open(input).unwrap(); + let metadata = file.metadata().unwrap(); + + let mut reader = BufReader::new(file); + let mut list: Vec = Vec::new(); + + // Считываем заголовок файла + let mut header_buffer = [0u8; 16]; + reader.seek(SeekFrom::Start(0)).unwrap(); + reader.read_exact(&mut header_buffer).unwrap(); + + let file_header = FileHeader { + size: LittleEndian::read_u32(&header_buffer[12..16]), + total: LittleEndian::read_u32(&header_buffer[8..12]), + type1: LittleEndian::read_u32(&header_buffer[0..4]), + type2: LittleEndian::read_u32(&header_buffer[4..8]), + }; + + if file_header.type1 != 1936020046 || file_header.type2 != 256 { + panic!("this isn't NRes file"); + } + + if metadata.len() != file_header.size as u64 { + panic!("incorrect size") + } + + // Считываем список файлов + let list_files_start_position = file_header.size - (file_header.total * 64); + let list_files_size = file_header.total * 64; + + let mut list_buffer = vec![0u8; list_files_size as usize]; + reader + .seek(SeekFrom::Start(list_files_start_position as u64)) + .unwrap(); + reader.read_exact(&mut list_buffer).unwrap(); + + if list_buffer.len() % 64 != 0 { + panic!("invalid files list") + } + + for i in 0..(list_buffer.len() / 64) { + let from = i * 64; + let to = (i * 64) + 64; + let chunk: &[u8] = &list_buffer[from..to]; + + let element_list = ListElement { + extension: String::from_utf8_lossy(&chunk[0..4]) + .trim_matches(char::from(0)) + .to_string(), + index: LittleEndian::read_u32(&chunk[60..64]), + name: String::from_utf8_lossy(&chunk[20..56]) + .trim_matches(char::from(0)) + .to_string(), + position: LittleEndian::read_u32(&chunk[56..60]), + size: LittleEndian::read_u32(&chunk[12..16]), + unknown0: LittleEndian::read_u32(&chunk[4..8]), + unknown1: LittleEndian::read_u32(&chunk[8..12]), + unknown2: LittleEndian::read_u32(&chunk[16..20]), + }; + + list.push(element_list) + } + + // Распаковываем файлы в директорию + for element in &list { + let path = format!("{}/{}.{}", output, element.name, element.index); + let mut file = File::create(path).unwrap(); + + let mut file_buffer = vec![0u8; element.size as usize]; + reader + .seek(SeekFrom::Start(element.position as u64)) + .unwrap(); + reader.read_exact(&mut file_buffer).unwrap(); + + file.write_all(&file_buffer).unwrap(); + file_buffer.clear(); + } + + // Выгрузка списка файлов в JSON + let path = format!("{}/{}", output, "index.json"); + let file = File::create(path).unwrap(); + let mut writer = BufWriter::new(file); + serde_json::to_writer_pretty(&mut writer, &list).unwrap(); + writer.flush().unwrap(); +} diff --git a/unpacker/Cargo.toml b/unpacker/Cargo.toml deleted file mode 100644 index adb64ec..0000000 --- a/unpacker/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "unpacker" -version = "0.1.1" -edition = "2021" - -[dependencies] -byteorder = "1.4.3" -serde = { version = "1.0.160", features = ["derive"] } -serde_json = "1.0.96" diff --git a/unpacker/README.md b/unpacker/README.md deleted file mode 100644 index 2c6be02..0000000 --- a/unpacker/README.md +++ /dev/null @@ -1,41 +0,0 @@ -# NRes Game Resource Unpacker - -At the moment, this is a demonstration of the NRes game resource unpacking algorithm in action. -It unpacks 100% of the NRes game resources for the game "Parkan: Iron Strategy". -The unpacked resources can be packed again using the [packer](../packer) utility and replace the original game files. - -__Attention!__ -This is a test version of the utility. -It overwrites existing files without asking. - -## Building - -To build the tools, you need to run the following command in the root directory: - -```bash -cargo build --release -``` - -## Running - -You can run the utility with the following command: - -```bash -./target/release/unpacker /path/to/file.ex /path/to/output -``` - -- `/path/to/file.ex`: This is the file containing the game resources that will be unpacked. -- `/path/to/output`: This is the directory where the unpacked files will be placed. - -## How it Works - -The structure describing the packed game resources is not fully understood yet. -Therefore, the utility saves unpacked files in the format `file_name.file_index` because some files have the same name. - -Additionally, an `index.json` file is created, which is important for re-packing the files. -This file lists all the fields that game resources have in their packed form. -It is essential to preserve the file index for the game to function correctly, as the game engine looks for the necessary files by index. - -Files can be replaced and packed back using the [packer](../packer). -The newly obtained game resource files are correctly processed by the game engine. -For example, sounds and 3D models of warbots' weapons were successfully replaced. diff --git a/unpacker/src/main.rs b/unpacker/src/main.rs deleted file mode 100644 index 2a84688..0000000 --- a/unpacker/src/main.rs +++ /dev/null @@ -1,124 +0,0 @@ -use std::env; -use std::fs::File; -use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write}; - -use byteorder::{ByteOrder, LittleEndian}; -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize, Debug)] -pub struct FileHeader { - pub size: u32, - pub total: u32, - pub type1: u32, - pub type2: u32, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct ListElement { - pub extension: String, - pub index: u32, - pub name: String, - #[serde(skip_serializing)] - pub position: u32, - #[serde(skip_serializing)] - pub size: u32, - pub unknown0: u32, - pub unknown1: u32, - pub unknown2: u32, -} - -fn main() { - let args: Vec = env::args().collect(); - - let input = &args[1]; - let output = &args[2]; - - unpack(String::from(input), String::from(output)); -} - -fn unpack(input: String, output: String) { - let file = File::open(input).unwrap(); - let metadata = file.metadata().unwrap(); - - let mut reader = BufReader::new(file); - let mut list: Vec = Vec::new(); - - // Считываем заголовок файла - let mut header_buffer = [0u8; 16]; - reader.seek(SeekFrom::Start(0)).unwrap(); - reader.read_exact(&mut header_buffer).unwrap(); - - let file_header = FileHeader { - size: LittleEndian::read_u32(&header_buffer[12..16]), - total: LittleEndian::read_u32(&header_buffer[8..12]), - type1: LittleEndian::read_u32(&header_buffer[0..4]), - type2: LittleEndian::read_u32(&header_buffer[4..8]), - }; - - if file_header.type1 != 1936020046 || file_header.type2 != 256 { - panic!("this isn't NRes file"); - } - - if metadata.len() != file_header.size as u64 { - panic!("incorrect size") - } - - // Считываем список файлов - let list_files_start_position = file_header.size - (file_header.total * 64); - let list_files_size = file_header.total * 64; - - let mut list_buffer = vec![0u8; list_files_size as usize]; - reader - .seek(SeekFrom::Start(list_files_start_position as u64)) - .unwrap(); - reader.read_exact(&mut list_buffer).unwrap(); - - if list_buffer.len() % 64 != 0 { - panic!("invalid files list") - } - - for i in 0..(list_buffer.len() / 64) { - let from = i * 64; - let to = (i * 64) + 64; - let chunk: &[u8] = &list_buffer[from..to]; - - let element_list = ListElement { - extension: String::from_utf8_lossy(&chunk[0..4]) - .trim_matches(char::from(0)) - .to_string(), - index: LittleEndian::read_u32(&chunk[60..64]), - name: String::from_utf8_lossy(&chunk[20..56]) - .trim_matches(char::from(0)) - .to_string(), - position: LittleEndian::read_u32(&chunk[56..60]), - size: LittleEndian::read_u32(&chunk[12..16]), - unknown0: LittleEndian::read_u32(&chunk[4..8]), - unknown1: LittleEndian::read_u32(&chunk[8..12]), - unknown2: LittleEndian::read_u32(&chunk[16..20]), - }; - - list.push(element_list) - } - - // Распаковываем файлы в директорию - for element in &list { - let path = format!("{}/{}.{}", output, element.name, element.index); - let mut file = File::create(path).unwrap(); - - let mut file_buffer = vec![0u8; element.size as usize]; - reader - .seek(SeekFrom::Start(element.position as u64)) - .unwrap(); - reader.read_exact(&mut file_buffer).unwrap(); - - file.write_all(&file_buffer).unwrap(); - file_buffer.clear(); - } - - // Выгрузка списка файлов в JSON - let path = format!("{}/{}", output, "index.json"); - let file = File::create(path).unwrap(); - let mut writer = BufWriter::new(file); - serde_json::to_writer_pretty(&mut writer, &list).unwrap(); - writer.flush().unwrap(); -} -- cgit v1.2.3