From 8d8653133bf3a12ac58c0e4f34624e9beac11751 Mon Sep 17 00:00:00 2001
From: Valentin Popov <valentin@popov.link>
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

---
 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 ++++++++++++++++++++++++
 9 files changed, 454 insertions(+)
 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

(limited to 'tools')

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<String>,
+    },
+    /// 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<String>) -> 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<indicatif::ProgressStyle> {
+    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<u8> = Vec::new();
+    file.read_to_end(&mut buffer)?;
+
+    // Декодируем метаданные
+    let mut cursor = std::io::Cursor::new(&buffer[4..]);
+    let img_width = cursor.read_u32::<byteorder::LittleEndian>()?;
+    let img_height = cursor.read_u32::<byteorder::LittleEndian>()?;
+
+    // Пропустить оставшиеся байты метаданных
+    cursor.set_position(20);
+
+    // Извлекаем данные изображения
+    let image_data = buffer[cursor.position() as usize..].to_vec();
+    let img =
+        image::ImageBuffer::<Rgba<u8>, _>::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<String> = 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<String> = 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<ListElement> = 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