Skip to content

Commit

Permalink
Support audio files
Browse files Browse the repository at this point in the history
  • Loading branch information
ByteZ1337 committed Jun 14, 2024
1 parent 8d10b48 commit 9e42ca5
Show file tree
Hide file tree
Showing 12 changed files with 240 additions and 109 deletions.
2 changes: 2 additions & 0 deletions src/command/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ pub struct AssetMetadata {

pub type ArtHeader = Vec<AssetMetadata>;

pub const DATA_FOLDER_NAME: &str = "PapersPlease_Data";

pub mod pack;
pub mod unpack;
pub mod patch;
Expand Down
25 changes: 14 additions & 11 deletions src/command/patch/assets_patcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ use walkdir::WalkDir;
use crate::command::pack;
use crate::command::patch::xml_patcher;
use crate::command::unpack::RepackInfo;
use crate::io_ext::WriteExt;
use crate::unity::{AssetsFile, AssetsFileContent, AssetsFileHeader, ObjectInfo};
use crate::unity::util::{AlignedString, AlignmentArgs};

/// Length of the header of the Art.dat object.
/// The header consists of:
Expand Down Expand Up @@ -201,23 +201,26 @@ fn pack_to_assets(temp_dir: &PathBuf, game_dir: &PathBuf, repack: RepackInfo) ->
write_zeroes(&mut writer, pad).context("Failed to write padding zeroes")?;
}

if obj.path_id != repack.art_path_id {
original.seek(SeekFrom::Start(original_file_offset + old_obj.byte_start))
.context("Failed to seek to object in original assets file")?;
let mut data = vec![0; obj.byte_size as usize];
original.read_exact(&mut data)
.context("Failed to read object data from original assets file")?;
writer.write_all(&data)?;
} else {
writer.write_dyn_string("Art.dat", &new_assets.header.endianness)
if obj.path_id == repack.art_path_id {
AlignedString("Art.dat".to_string()).write_options(&mut writer, new_assets.endian(), AlignmentArgs::new(4))
.context("Failed to write object name")?;
writer.write_u32_order(&new_assets.header.endianness, new_art_len as u32)
(new_art_len as u32).write_options(&mut writer, new_assets.endian(), ())
.context("Failed to write object data length")?;
// copy over the new art file
let mut art_file = BufReader::new(File::open(&temp_art)
.context("Failed to open temp art file")?);
std::io::copy(&mut art_file, &mut writer)
.context("Failed to copy new art file to assets file")?;
} else if let Some(audio) = repack.audio_assets.get(&obj.path_id) {
audio.write_options(&mut writer, new_assets.endian(), ())
.context("Failed to write audio object")?;
} else {
original.seek(SeekFrom::Start(original_file_offset + old_obj.byte_start))
.context("Failed to seek to object in original assets file")?;
let mut data = vec![0; obj.byte_size as usize];
original.read_exact(&mut data)
.context("Failed to read object data from original assets file")?;
writer.write_all(&data)?;
}
}

Expand Down
104 changes: 104 additions & 0 deletions src/command/patch/audio_patcher.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
use std::{fs, io};
use std::collections::HashMap;
use std::ffi::OsStr;
use std::fs::File;
use std::io::BufWriter;
use std::path::PathBuf;

use anyhow::Context;
use binrw::io::BufReader;
use serde::{Deserialize, Serialize};

use crate::command::unpack::RepackInfo;
use crate::unity::audio::{AudioClip, AudioCompressionFormat, StreamedResource};
use crate::unity::util::{AlignedString, U8Bool};

type AudioPatchList = Vec<AudioPatch>;

/// If you rename this for some reason, make sure it has a length between 21 and 24 characters
/// (inclusive) to avoid changing the size of the audio clips in the assets file.
const MODDED_RESOURCES_FILE: &str = "modded_assets0.resource";

#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AudioPatch {
pub object_name: String,
pub patched_path: PathBuf,
pub load_type: i32,
pub channels: i32,
pub frequency: i32,
pub bits_per_sample: i32,
pub length: f32,
pub is_tracker_format: bool,
pub subsound_index: i32,
pub preload_audio_data: bool,
pub load_in_background: bool,
pub legacy3d: bool,
pub compression_format: AudioCompressionFormat,
}

pub fn patch_audio(audio_patches_path: &PathBuf, game_dir: &PathBuf, repack_info: &mut RepackInfo) -> anyhow::Result<()> {
let audio_patches = fs::read_to_string(audio_patches_path)
.context("Failed to read audio patches file")?;
let audio_patches: AudioPatchList = serde_json::from_str(&audio_patches)
.context("Failed to parse audio patches file")?;

let by_object_name = repack_info.audio_assets.iter()
.map(|(path_id, clip)| (clip.object_name.as_str(), (path_id.clone(), clip)))
.collect::<HashMap<_, _>>();

let mut patched_clips: HashMap<i64, AudioClip> = HashMap::new();
let mut modded_audio_writer = BufWriter::new(File::create(
game_dir.join(MODDED_RESOURCES_FILE)
).context("Failed to create modded audio file")?);
let patches_dir = audio_patches_path.parent()
.context("Failed to get parent directory of audio patches file")?;

let mut offset = 0u64;
for patch in &audio_patches {
if patch.patched_path.extension() != Some(OsStr::new("fsb")) {
anyhow::bail!("Only FSB files are supported for audio patches.");
}

if let Some((path_id, clip)) = by_object_name.get(patch.object_name.as_str()) {
let mut reader = BufReader::new(File::open(patches_dir.join(&patch.patched_path))
.context("Failed to open patched audio file")?);
let written = io::copy(&mut reader, &mut modded_audio_writer)
.context("Failed to copy patched audio file to modded audio file")?;

let new_clip = AudioClip {
object_name: clip.object_name.clone(),
resource: StreamedResource {
source: AlignedString(MODDED_RESOURCES_FILE.to_string()),
offset: offset as i64,
size: written as i64,
},
load_type: patch.load_type,
channels: patch.channels,
frequency: patch.frequency,
bits_per_sample: patch.bits_per_sample,
length: patch.length,
is_tracker_format: U8Bool(patch.is_tracker_format),
subsound_index: patch.subsound_index,
preload_audio_data: U8Bool(patch.preload_audio_data),
load_in_background: U8Bool(patch.load_in_background),
legacy3d: U8Bool(patch.legacy3d),
compression_format: patch.compression_format.clone(),
};
patched_clips.insert(*path_id, new_clip);

offset += written;
} else {
let mut available = by_object_name.keys().map(|s| s.to_string()).collect::<Vec<_>>();
available.sort();
anyhow::bail!("Audio name {} in audio patches does not exist in the assets file. Available audio names:\n{}",
patch.object_name,
available.join(", ")
);
}
}

repack_info.audio_assets = patched_clips;

Ok(())
}
21 changes: 14 additions & 7 deletions src/command/patch/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,20 @@ use std::path::PathBuf;

use anyhow::Context;
use rand::random;
use unpack::unpack_assets;

use crate::{I18nCompatMode, NewArgs};
use crate::{I18nCompatMode, Args};
use crate::command::patch::assets_patcher::patch_assets;
use crate::command::patch::audio_patcher::patch_audio;
use crate::command::patch::locale_patcher::patch_locale;
use crate::command::unpack;
use crate::command::{DATA_FOLDER_NAME, unpack};

mod assets_patcher;
mod xml_patcher;
mod locale_patcher;
pub mod audio_patcher;

pub fn patch(args: &NewArgs, patch: &PathBuf, locale_mode: &I18nCompatMode) -> anyhow::Result<()> {
pub fn patch(args: &Args, patch: &PathBuf, locale_mode: &I18nCompatMode) -> anyhow::Result<()> {
println!("Patching assets with {:?} with locale mode {:?}", patch, locale_mode);

if !patch.is_dir() {
Expand All @@ -27,8 +30,13 @@ pub fn patch(args: &NewArgs, patch: &PathBuf, locale_mode: &I18nCompatMode) -> a
let temp_unpacked = temp_dir.join("unpacked");
fs::create_dir_all(&temp_unpacked)
.context("Failed to create temp directory")?;
let repack_info = unpack::unpack_assets(args, &game_files.assets, &temp_unpacked)?;
let audio_patches = patch.join("audio_patches.json");
let process_audio = audio_patches.is_file();

let mut repack_info = unpack_assets(args, &game_files.assets, &temp_unpacked, process_audio)?;
if process_audio {
patch_audio(&audio_patches, &game_files.game_dir, &mut repack_info)?;
}
let patched_dir = patch_assets(patch, &temp_dir, &game_files.game_dir, repack_info)?;

if locale_mode == &I18nCompatMode::Normal {
Expand All @@ -41,7 +49,6 @@ pub fn patch(args: &NewArgs, patch: &PathBuf, locale_mode: &I18nCompatMode) -> a
Ok(())
}


//<editor-fold desc="Filesystem preparations" defaultstate="collapsed">
pub struct GameFiles {
pub game_dir: PathBuf,
Expand All @@ -52,10 +59,10 @@ pub struct GameFiles {

fn prepare_game_files(game_dir: &PathBuf) -> anyhow::Result<GameFiles> {
// if game_dir is not already PapersPlease_Data, append it
let game_dir = if game_dir.ends_with("PapersPlease_Data") {
let game_dir = if game_dir.ends_with(DATA_FOLDER_NAME) {
game_dir.clone()
} else {
game_dir.join("PapersPlease_Data")
game_dir.join(DATA_FOLDER_NAME)
};

if !game_dir.is_dir() {
Expand Down
5 changes: 3 additions & 2 deletions src/command/revert.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
use std::path::PathBuf;
use crate::command::DATA_FOLDER_NAME;

pub fn revert(game_dir: &PathBuf) -> anyhow::Result<()> {
// if game_dir is not already PapersPlease_Data, append it
let game_dir = if game_dir.ends_with("PapersPlease_Data") {
let game_dir = if game_dir.ends_with(DATA_FOLDER_NAME) {
game_dir.clone()
} else {
game_dir.join("PapersPlease_Data")
game_dir.join(DATA_FOLDER_NAME)
};

if !game_dir.is_dir() {
Expand Down
47 changes: 31 additions & 16 deletions src/command/unpack.rs
Original file line number Diff line number Diff line change
@@ -1,27 +1,28 @@
use std::collections::HashMap;
use std::ffi::OsStr;
use std::fs::File;
use std::io::{BufWriter, Read, Seek, SeekFrom};
use std::path::{Path, PathBuf};
use anyhow::Context;

use anyhow::Context;
use binrw::BinRead;
use binrw::io::BufReader;

use crate::{crypto, NewArgs};
use crate::command::ArtHeader;
use crate::io_ext::ReadExt;
use crate::{crypto, Args, unity};
use crate::command::{ArtHeader, DATA_FOLDER_NAME};
use crate::unity::AssetsFile;
use crate::unity::audio::AudioClip;
use crate::unity::util::{AlignedString, AlignmentArgs};

pub fn unpack(args: &NewArgs, input: &Option<PathBuf>, output: &PathBuf) -> anyhow::Result<()> {
pub fn unpack(args: &Args, input: &Option<PathBuf>, output: &PathBuf) -> anyhow::Result<()> {
let input = &find_input(args, input)?;
let extension = input.extension();
match extension {
Some(ext) => {
if ext == OsStr::new("dat") || ext == OsStr::new("txt") {
if ext == OsStr::new("dat") || ext == OsStr::new("txt") {
unpack_dat(args, input, output)?;
} else if ext == OsStr::new("assets") {
unpack_assets(args, input, output)?;
unpack_assets(args, input, output, false)?;
} else {
anyhow::bail!("Input file has an invalid extension. (Supported: .dat, .assets)");
}
Expand All @@ -34,7 +35,7 @@ pub fn unpack(args: &NewArgs, input: &Option<PathBuf>, output: &PathBuf) -> anyh
Ok(())
}

fn find_input(args: &NewArgs, input: &Option<PathBuf>) -> anyhow::Result<PathBuf> {
fn find_input(args: &Args, input: &Option<PathBuf>) -> anyhow::Result<PathBuf> {
match input {
// Check if an input path was provided
Some(path) => {
Expand All @@ -44,9 +45,11 @@ fn find_input(args: &NewArgs, input: &Option<PathBuf>) -> anyhow::Result<PathBuf
Ok(path.clone())
}
None => {
let assets = args.game_dir
.join("PapersPlease_Data")
.join("sharedassets0.assets");
let mut assets = args.game_dir.clone();
if !assets.ends_with(DATA_FOLDER_NAME) {
assets.push(DATA_FOLDER_NAME);
}
assets.push("sharedassets0.assets");

if assets.is_file() {
Ok(assets)
Expand All @@ -57,7 +60,7 @@ fn find_input(args: &NewArgs, input: &Option<PathBuf>) -> anyhow::Result<PathBuf
}
}

pub fn unpack_dat(args: &NewArgs, input: &PathBuf, output: &PathBuf) -> anyhow::Result<()> {
pub fn unpack_dat(args: &Args, input: &PathBuf, output: &PathBuf) -> anyhow::Result<()> {
let mut data = std::fs::read(input)
.context("Failed to read input file")?;
println!("Unpacking assets from: {}", input.display());
Expand Down Expand Up @@ -106,12 +109,13 @@ pub fn unpack_dat(args: &NewArgs, input: &PathBuf, output: &PathBuf) -> anyhow::

pub struct RepackInfo {
pub assets: AssetsFile,
pub art_path_id: i64,
pub audio_assets: HashMap<i64, AudioClip>,
pub art_key: String,
pub art_path_id: i64,
pub original_assets: PathBuf,
}

pub fn unpack_assets(args: &NewArgs, input_path: &PathBuf, output: &PathBuf) -> anyhow::Result<RepackInfo> {
pub fn unpack_assets(args: &Args, input_path: &PathBuf, output: &PathBuf, process_audio: bool) -> anyhow::Result<RepackInfo> {
let input = File::open(input_path)
.context("Failed to open input file")?;
let mut input = BufReader::new(input);
Expand All @@ -122,8 +126,9 @@ pub fn unpack_assets(args: &NewArgs, input_path: &PathBuf, output: &PathBuf) ->

let mut art_file: Option<PathBuf> = None;
let mut art_path_id: Option<i64> = None;
let mut audio_assets = HashMap::new();
for obj in objects {
if obj.class_id == 49 { // text asset
if obj.class_id == unity::TEXT_ASSET_CLASS { // text asset
input.seek(SeekFrom::Start(assets.header.offset_first_file + obj.byte_start))
.context("Failed to seek to object")?;
let name = AlignedString::read_options(&mut input, assets.endian(), AlignmentArgs::new(4))
Expand All @@ -146,8 +151,17 @@ pub fn unpack_assets(args: &NewArgs, input_path: &PathBuf, output: &PathBuf) ->

art_file = Some(temp);
art_path_id = Some(obj.path_id);
break;
if !process_audio {
break;
}
}
} else if process_audio && obj.class_id == unity::AUDIO_CLIP_CLASS {
input.seek(SeekFrom::Start(assets.header.offset_first_file + obj.byte_start))
.context("Failed to seek to object")?;

let audio_clip = AudioClip::read_options(&mut input, assets.endian(), ())
.context("Failed to read AudioClip object")?;
audio_assets.insert(obj.path_id, audio_clip);
}
}

Expand All @@ -160,6 +174,7 @@ pub fn unpack_assets(args: &NewArgs, input_path: &PathBuf, output: &PathBuf) ->
// Any unwraps here are safe because None values would've resulted in earlier bail
Ok(RepackInfo {
assets,
audio_assets,
art_path_id: art_path_id.unwrap(),
art_key: args.art_key.clone().unwrap(),
original_assets: input_path.clone(),
Expand Down
15 changes: 10 additions & 5 deletions src/crypto.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use std::fs::File;
use std::io::{Read, Seek};
use std::path::Path;
use std::slice;

use crate::{NewArgs};
use crate::command::DATA_FOLDER_NAME;
use crate::Args;

const KEY_OFFSET: usize = 0x39420;

Expand Down Expand Up @@ -70,12 +70,17 @@ fn as_u32_slice_mut(x: &mut [u8]) -> &mut [u32] {
unsafe { slice::from_raw_parts_mut(x.as_mut_ptr() as *mut u32, x.len() / 4) }
}

pub fn extract_key(args: &NewArgs) -> anyhow::Result<String> {
let game_dir = Path::new(&args.game_dir);
pub fn extract_key(args: &Args) -> anyhow::Result<String> {
let mut game_dir = args.game_dir.clone();
if !game_dir.exists() || !game_dir.is_dir() {
anyhow::bail!("Game directory not found: {}", game_dir.display());
}
let global_metadata = game_dir.join("PapersPlease_Data")

if !game_dir.ends_with(DATA_FOLDER_NAME) {
game_dir.push(DATA_FOLDER_NAME);
}

let global_metadata = game_dir
.join("il2cpp_data")
.join("Metadata")
.join("global-metadata.dat");
Expand Down
Loading

0 comments on commit 9e42ca5

Please sign in to comment.