Обновление структуры проекта

This commit is contained in:
2025-02-08 01:11:02 +00:00
parent 94d2f8a512
commit 8d8653133b
19 changed files with 27 additions and 79 deletions

14
tools/nres-cli/Cargo.toml Normal file
View File

@ -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"

6
tools/nres-cli/README.md Normal file
View File

@ -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.

198
tools/nres-cli/src/main.rs Normal file
View File

@ -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()
}

View File

@ -0,0 +1,8 @@
[package]
name = "texture-decoder"
version = "0.1.0"
edition = "2021"
[dependencies]
byteorder = "1.4.3"
image = "0.25.0"

View File

@ -0,0 +1,13 @@
# Декодировщик текстур
Сборка:
```bash
cargo build --release
```
Запуск:
```bash
./target/release/texture-decoder ./out/AIM_02.0 ./out/AIM_02.0.png
```

View File

@ -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)
}
}

View File

@ -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"

41
tools/unpacker/README.md Normal file
View File

@ -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.

124
tools/unpacker/src/main.rs Normal file
View File

@ -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();
}