Skip to content

Commit 260e930

Browse files
committed
patching to unity .assets file
1 parent 27a1a6f commit 260e930

File tree

3 files changed

+127
-14
lines changed

3 files changed

+127
-14
lines changed

src/command/patch/assets_patcher.rs

+118-6
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,35 @@
11
use std::ffi::OsStr;
2+
use std::fs::File;
3+
use std::io::{BufWriter, Read, Seek, SeekFrom, Write};
24
use std::path::{Path, PathBuf};
5+
36
use anyhow::Context;
7+
use binrw::__private::write_zeroes;
8+
use binrw::BinWrite;
9+
use binrw::io::BufReader;
10+
use byteorder::WriteBytesExt;
411
use walkdir::WalkDir;
12+
13+
use crate::command::pack;
514
use crate::command::patch::xml_patcher;
15+
use crate::command::unpack::RepackInfo;
16+
use crate::io_ext::WriteExt;
17+
use crate::unity::{AssetsFile, AssetsFileContent, AssetsFileHeader, ObjectInfo};
18+
19+
/// Length of the header of the Art.dat object.
20+
/// The header consists of:
21+
/// - 4 bytes for object name length
22+
/// - 7 bytes for object name (Art.dat)
23+
/// - 1 byte for field index
24+
/// - 4 bytes for data length
25+
const ART_OBJ_HEADER_LEN: u64 = 4 + 7 + 1 + 4;
626

7-
pub fn patch_assets(patch: &PathBuf, unpacked: &PathBuf, temp_dir: &PathBuf) -> anyhow::Result<PathBuf> {
27+
pub fn patch_assets(
28+
patch: &PathBuf,
29+
unpacked: &PathBuf,
30+
temp_dir: &PathBuf,
31+
repack_info: RepackInfo,
32+
) -> anyhow::Result<PathBuf> {
833
println!("Patching assets..");
934
let patched_assets = temp_dir.join("patched");
1035
std::fs::create_dir_all(&patched_assets)
@@ -46,11 +71,10 @@ pub fn patch_assets(patch: &PathBuf, unpacked: &PathBuf, temp_dir: &PathBuf) ->
4671
copy_file(&patch_file.as_path(), rel_path, &patched_assets)?;
4772
} else if ext == OsStr::new("xml") || ext == OsStr::new("fnt") {
4873
println!("Patching xml file: {}", rel_path.display());
49-
patch_xml(&file, &patch_file, rel_path, &patched_assets)?;
74+
patch_xml(&file.path(), &patch_file, rel_path, &patched_assets)?;
5075
} else {
5176
anyhow::bail!("Unsupported file type: {}", patch_file.display());
5277
}
53-
5478
}
5579

5680
// Loop over any files newly added with the patch
@@ -79,7 +103,7 @@ pub fn patch_assets(patch: &PathBuf, unpacked: &PathBuf, temp_dir: &PathBuf) ->
79103
}
80104
}
81105

82-
return Ok(patched_assets);
106+
pack_to_assets(temp_dir, &patched_assets, repack_info)
83107
}
84108

85109
/// Copies a file from one of the input directories to the patched assets directory and makes sure
@@ -94,11 +118,99 @@ fn copy_file(file: &Path, rel_path: &Path, patched_assets: &PathBuf) -> anyhow::
94118
}
95119

96120
/// Patches an XML file using the given patch file and writes the output to the patched assets directory
97-
fn patch_xml(original: &walkdir::DirEntry, patch_file: &PathBuf, rel_path: &Path, patched_assets: &PathBuf) -> anyhow::Result<()> {
121+
fn patch_xml(original: &Path, patch_file: &PathBuf, rel_path: &Path, patched_assets: &PathBuf) -> anyhow::Result<()> {
98122
let output = patched_assets.join(rel_path);
99123
if let Some(parent) = output.parent() {
100124
std::fs::create_dir_all(parent)
101125
.context("Failed to create directory")?;
102126
}
103-
xml_patcher::patch(original.path(), patch_file, &output)
127+
xml_patcher::patch(original, patch_file, &output)
128+
}
129+
130+
fn pack_to_assets(temp_dir: &PathBuf, patched: &PathBuf, repack: RepackInfo) -> anyhow::Result<PathBuf> {
131+
let output = temp_dir.join("repacked.assets");
132+
let temp_art = temp_dir.join("patched-art.dat");
133+
pack::pack(&repack.art_key, &Some(patched.clone()), &temp_art)?;
134+
let assets = repack.assets;
135+
let new_art_len = std::fs::metadata(&temp_art)
136+
.context("Failed to get metadata of temp art file")?
137+
.len();
138+
139+
// header
140+
let mut header = AssetsFileHeader { file_size: 0, ..assets.header };
141+
142+
// content
143+
let mut objects = Vec::new();
144+
let mut current_offset = 0;
145+
for obj in &assets.content.objects {
146+
if current_offset % 4 != 0 {
147+
current_offset += 4 - (current_offset % 4);
148+
}
149+
150+
let mut new_object = ObjectInfo {
151+
path_id: obj.path_id,
152+
byte_start: current_offset,
153+
byte_size: 0,
154+
type_id: obj.type_id,
155+
};
156+
if obj.path_id == repack.art_path_id {
157+
new_object.byte_size = (new_art_len + ART_OBJ_HEADER_LEN) as u32;
158+
} else {
159+
new_object.byte_size = obj.byte_size;
160+
}
161+
current_offset += new_object.byte_size as u64;
162+
objects.push(new_object);
163+
}
164+
header.file_size = header.offset_first_file + current_offset;
165+
let content = AssetsFileContent { objects, ..assets.content };
166+
let new_assets = AssetsFile { header, content };
167+
let mut writer = BufWriter::new(File::create(&output)
168+
.context("Failed to create output file")?);
169+
new_assets.write(&mut writer)
170+
.context("Failed to write assets file header")?;
171+
172+
// pad with zeroes until first file offset is reached (yes this is also what Unity does)
173+
let pad = assets.header.offset_first_file - writer.seek(SeekFrom::Current(0))
174+
.context("Failed to get current position in output file")?;
175+
write_zeroes(&mut writer, pad)?;
176+
177+
// write the actual object data
178+
let mut original = BufReader::new(File::open(&repack.original_assets)
179+
.context("Failed to open original assets file")?);
180+
let original_file_offset = &assets.header.offset_first_file;
181+
for (obj, old_obj) in new_assets.content.objects.iter().zip(assets.content.objects) {
182+
183+
let pos = writer.stream_position()
184+
.context("Failed to get current position in output file")?;
185+
if pos != obj.byte_start + original_file_offset {
186+
// pad with zeroes until the object's start offset is reached
187+
let pad = obj.byte_start + original_file_offset - pos;
188+
write_zeroes(&mut writer, pad).context("Failed to write padding zeroes")?;
189+
}
190+
191+
if obj.path_id != repack.art_path_id {
192+
original.seek(SeekFrom::Start(original_file_offset + old_obj.byte_start))
193+
.context("Failed to seek to object in original assets file")?;
194+
let mut data = vec![0; obj.byte_size as usize];
195+
original.read_exact(&mut data)
196+
.context("Failed to read object data from original assets file")?;
197+
writer.write_all(&data)?;
198+
} else {
199+
writer.write_dyn_string("Art.dat", &new_assets.header.endianness)
200+
.context("Failed to write object name")?;
201+
writer.write_u8(0)
202+
.context("Failed to write field index")?;
203+
writer.write_u32_order(&new_assets.header.endianness, new_art_len as u32)
204+
.context("Failed to write object data length")?;
205+
// copy over the new art file
206+
let mut art_file = BufReader::new(File::open(&temp_art)
207+
.context("Failed to open temp art file")?);
208+
std::io::copy(&mut art_file, &mut writer)
209+
.context("Failed to copy new art file to assets file")?;
210+
}
211+
212+
}
213+
214+
println!("Packed objets to: {}", output.display());
215+
Ok(output)
104216
}

src/command/patch/mod.rs

+7-6
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
pub mod assets_patcher;
2-
pub mod xml_patcher;
3-
41
use std::env::temp_dir;
52
use std::path::PathBuf;
6-
use anyhow::Context;
73

4+
use anyhow::Context;
85
use rand::random;
96

107
use crate::{I18nCompatMode, NewArgs};
118
use crate::command::patch::assets_patcher::patch_assets;
129
use crate::command::unpack;
1310

11+
pub mod assets_patcher;
12+
pub mod xml_patcher;
13+
1414
pub fn patch(args: &NewArgs, patch: &PathBuf, locale_mode: &I18nCompatMode) -> anyhow::Result<()> {
1515
println!("Patching assets with {:?} with locale mode {:?}", patch, locale_mode);
1616

@@ -26,9 +26,9 @@ pub fn patch(args: &NewArgs, patch: &PathBuf, locale_mode: &I18nCompatMode) -> a
2626
std::fs::create_dir_all(&temp_unpacked)
2727
.context("Failed to create temp directory")?;
2828

29-
unpack::unpack_assets(args, &game_files.assets, &temp_unpacked)?;
29+
let repack_info = unpack::unpack_assets(args, &game_files.assets, &temp_unpacked)?;
3030

31-
patch_assets(patch, &temp_unpacked, &temp_dir)?;
31+
patch_assets(patch, &temp_unpacked, &temp_dir, repack_info)?;
3232

3333
Ok(())
3434
}
@@ -82,6 +82,7 @@ fn prepare_file(game_dir: &PathBuf, name: &str) -> anyhow::Result<PathBuf> {
8282

8383
fn create_temp_dir() -> PathBuf {
8484
let mut temp_dir = temp_dir();
85+
temp_dir.push("papers-tools");
8586
temp_dir.push(format!("papers_please_assets_{}", random::<u64>()));
8687
temp_dir
8788
}

src/unity/mod.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ pub struct SerializedType {
9494
#[derive(Debug, PartialEq)]
9595
pub struct ScriptType {
9696
local_serialized_file_index: i32,
97-
#[br(align_before(4))]
97+
#[brw(align_before(4))]
9898
local_identifier_in_file: i64,
9999
}
100100

@@ -110,7 +110,7 @@ pub struct FileIdentifier {
110110
#[binrw]
111111
#[derive(Debug, PartialEq)]
112112
pub struct ObjectInfo {
113-
#[br(align_before(4))]
113+
#[brw(align_before(4))]
114114
pub path_id: i64,
115115
pub byte_start: u64,
116116
pub byte_size: u32,

0 commit comments

Comments
 (0)