Skip to content

Commit

Permalink
📝 add more docs
Browse files Browse the repository at this point in the history
  • Loading branch information
nicks96432 committed Feb 4, 2025
1 parent 7848829 commit 65a13e0
Show file tree
Hide file tree
Showing 7 changed files with 150 additions and 34 deletions.
15 changes: 11 additions & 4 deletions src/bin/mltd/extract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,13 @@ pub async fn extract_files(args: &ExtractorArgs) -> Result<(), Error> {
.map(|i| AssetRipper::new(&args.asset_ripper_path, port_start + i))
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.map(|a| Mutex::new(a))
.map(Mutex::new)
.collect::<Vec<_>>();

let files = args
.input_paths
.iter()
.filter_map(|p| match glob::glob(&p) {
.filter_map(|p| match glob::glob(p) {
Ok(paths) => Some(paths),
Err(e) => {
log::warn!("failed to glob `{}`: {}", p, e);
Expand Down Expand Up @@ -127,6 +127,7 @@ pub async fn extract_files(args: &ExtractorArgs) -> Result<(), Error> {
Ok(())
}

/// Extracts a single .unity3d file.
async fn extract_file<P>(
path: &P,
asset_ripper: &mut AssetRipper,
Expand Down Expand Up @@ -157,6 +158,7 @@ where
Ok(())
}

/// Extracts a single asset according to its class.
async fn extract_asset(
_bundle_no: usize,
_collection_no: usize,
Expand All @@ -175,6 +177,9 @@ async fn extract_asset(
Ok(())
}

/// Extracts a TextAsset.
///
/// Audio assets are binary TextAsset, so they are handled here as well.
async fn extract_text_asset(
info: AssetInfo,
asset_ripper: &mut AssetRipper,
Expand All @@ -200,9 +205,10 @@ async fn extract_text_asset(

let file_path = tmpdir.path().join(info.original_path.as_ref().unwrap());
match &info.entry.2 {
// CRI .acb and .awb audio
n if n.ends_with(".acb") || n.ends_with(".awb") => {
// remove .bytes extension for vgmstream
std::fs::rename(&file_path, &file_path.with_extension(""))?;
std::fs::rename(&file_path, file_path.with_extension(""))?;
let file_path = file_path.with_extension("");

let output_path = args
Expand All @@ -220,7 +226,7 @@ async fn extract_text_asset(
tokio::task::spawn_blocking(move || {
let mut options = ffmpeg_next::Dictionary::new();
for (key, value) in &args {
options.set(&key, &value);
options.set(key, value);
}
if !args.is_empty() {
log::trace!("audio options: {:#?}", options);
Expand All @@ -235,6 +241,7 @@ async fn extract_text_asset(
})
.await??;
}
// AES-192-CBC encrypted plot text
n if n.ends_with(".gtx") => {
let output_path = args
.output
Expand Down
86 changes: 63 additions & 23 deletions src/mltd/extract/audio.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//! Audio transcoding.
use std::collections::VecDeque;
use std::ffi::{c_int, c_uint};
use std::path::Path;
Expand All @@ -8,42 +10,70 @@ use vgmstream::{Options, StreamFile, VgmStream};

use crate::Error;

/// An encoder that transcodes game audio to the target codec.
pub struct Encoder<'a> {
pub options: Option<ffmpeg_next::Dictionary<'a>>,

/// VgmStream stream file.
pub vgmstream: VgmStream,

/// Original audio channel layout.
pub from_channel_layout: ffmpeg_next::ChannelLayout,
/// Original sample format.
pub from_sample_format: ffmpeg_next::format::Sample,
/// Original sample rate.
pub from_sample_rate: i32,

/// FFmpeg encoder.
pub encoder: ffmpeg_next::codec::encoder::audio::Encoder,

/// FFmpeg encoder options.
pub options: Option<ffmpeg_next::Dictionary<'a>>,

/// FFmpeg output context.
pub output: ffmpeg_next::format::context::Output,

/// FFmpeg resampler context.
pub resampler: ffmpeg_next::software::resampling::Context,

/// FFmpeg audio frame.
pub frame: ffmpeg_next::frame::Audio,

/// Audio sample count.
pub sample_count: i64,

/// Audio next presentation timestamp.
pub next_pts: i64,

queue: VecDeque<u8>,
/// Fifo for audio data.
fifo: VecDeque<u8>,
}

impl<'a> Encoder<'a> {
/// Default audio frame size.
///
/// Some codecs accept variable frame sizes, and this is used for those codecs.
pub const DEFAULT_FRAME_SIZE: u32 = 4096;

/// Opens the encoder with the given parameters.
///
/// VgmStream will decode the input game audio, and FFmpeg will enocde with the given
/// codec and options. The output file will be truncated if it exists.
///
/// # Errors
///
/// [`Error::VGMStream`]: if vgmstream cannot identify the input file format.
///
/// [`Error::FFmpeg`]: if ffmpeg encoder initialization failed.
pub fn open<P>(
input: &P,
output: &P,
codec: &str,
options: Option<ffmpeg_next::Dictionary<'a>>,
input_file: &P,
output_dir: &P,
output_codec: &str,
output_options: Option<ffmpeg_next::Dictionary<'a>>,
) -> Result<Self, Error>
where
P: AsRef<Path> + ?Sized,
{
let mut vgmstream = VgmStream::new()?;
let sf = StreamFile::open(&vgmstream, input)?;
let sf = StreamFile::open(&vgmstream, input_file)?;
vgmstream.open_song(&mut Options {
libsf: &sf,
format_id: 0,
Expand All @@ -56,12 +86,12 @@ impl<'a> Encoder<'a> {

log::trace!("audio format: {:#?}", acb_fmt);

let mut output = match options {
Some(ref o) => ffmpeg_next::format::output_with(output, o.clone()),
None => ffmpeg_next::format::output(output.as_ref()),
let mut output = match output_options {
Some(ref o) => ffmpeg_next::format::output_with(output_dir, o.clone()),
None => ffmpeg_next::format::output(output_dir.as_ref()),
}?;

let codec = ffmpeg_next::encoder::find_by_name(codec)
let codec = ffmpeg_next::encoder::find_by_name(output_codec)
.ok_or(Error::Generic("Failed to find encoder".to_owned()))?;

let mut encoder = ffmpeg_next::codec::Context::new_with_codec(codec).encoder().audio()?;
Expand All @@ -86,7 +116,7 @@ impl<'a> Encoder<'a> {
encoder.set_flags(flag | ffmpeg_next::codec::Flags::GLOBAL_HEADER);
}

let encoder = match options {
let encoder = match output_options {
Some(ref o) => encoder.open_with(o.clone()),
None => encoder.open(),
}?;
Expand Down Expand Up @@ -117,23 +147,26 @@ impl<'a> Encoder<'a> {
)?;

Ok(Self {
options,
vgmstream,
from_channel_layout,
from_sample_format,
from_sample_rate: acb_fmt.sample_rate,
encoder,
options: output_options,
output,
resampler,
frame,

sample_count: 0,
next_pts: 0,

queue: VecDeque::new(),
fifo: VecDeque::new(),
})
}

/// Encodes the next audio frame and writes the encoded packets to the output file.
///
/// Returns `false` if there is more audio data to encode.
fn write_frame(&mut self, eof: bool) -> Result<bool, Error> {
match eof {
false => self.encoder.send_frame(&self.frame),
Expand Down Expand Up @@ -187,18 +220,18 @@ impl<'a> Encoder<'a> {
break;
}

self.queue.extend(buf);
if self.queue.len() >= needed_len {
self.fifo.extend(buf);
if self.fifo.len() >= needed_len {
break;
}
}

let samples = match self.queue.len() {
let samples = match self.fifo.len() {
0 => return None,
len if len < needed_len => std::mem::take(&mut self.queue).into(),
len if len < needed_len => std::mem::take(&mut self.fifo).into(),
_ => {
let mut rest = self.queue.split_off(needed_len);
std::mem::swap(&mut rest, &mut self.queue);
let mut rest = self.fifo.split_off(needed_len);
std::mem::swap(&mut rest, &mut self.fifo);

Vec::from(rest)
}
Expand All @@ -223,6 +256,8 @@ impl<'a> Encoder<'a> {
}

/// Encodes the next audio frame.
///
/// Returns `false` if there is more audio data to encode.
fn write_audio_frame(&mut self) -> Result<bool, Error> {
if let Some(frame) = self.get_audio_frame() {
assert_eq!(self.resampler.delay(), None, "there should be no delay");
Expand Down Expand Up @@ -259,6 +294,11 @@ impl<'a> Encoder<'a> {
self.write_frame(true)
}

/// Encodes the audio streams.
///
/// # Errors
///
/// [`Error::FFmpeg`]: if encoding failed.
pub fn encode(&mut self) -> Result<(), Error> {
match self.options {
Some(ref o) => {
Expand Down Expand Up @@ -328,7 +368,8 @@ fn get_supported_formats(
}
}

/*
/// Returns a list of supported audio formats.
#[cfg(any())]
fn get_supported_formats_new(
encoder: &ffmpeg_next::codec::encoder::Encoder,
) -> Result<Vec<ffmpeg_next::format::Sample>, Error> {
Expand Down Expand Up @@ -358,7 +399,6 @@ fn get_supported_formats_new(
.map(|fmt| (*fmt).into())
.collect())
}
*/

fn choose_format(
supported_formats: &[ffmpeg_next::format::Sample],
Expand Down
2 changes: 2 additions & 0 deletions src/mltd/extract/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
//! Functions to extract assets from MLTD.
pub mod audio;
pub mod text;
45 changes: 45 additions & 0 deletions src/mltd/extract/text.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//! Encrypt and decrypt text assets in MLTD.
use aes::cipher::block_padding::Pkcs7;
use aes::cipher::inout::InOutBufReserved;
use aes::cipher::{BlockDecryptMut, BlockEncryptMut, KeyIvInit};
Expand All @@ -6,8 +8,16 @@ use cbc::{Decryptor, Encryptor};

use crate::Error;

/// The key used to derive [`MLTD_TEXT_DECRYPT_KEY`] and
/// [`MLTD_TEXT_DECRYPT_IV`].
pub const MLTD_TEXT_PBKDF2_HMAC_SHA1_KEY: &[u8; 8] = b"Millicon";

/// The salt used to derive [`MLTD_TEXT_DECRYPT_KEY`] and
/// [`MLTD_TEXT_DECRYPT_IV`].
pub const MLTD_TEXT_PBKDF2_HMAC_SHA1_SALT: &[u8; 9] = b"DAISUL___";

/// The number of iterations used to derive [`MLTD_TEXT_DECRYPT_KEY`] and
/// [`MLTD_TEXT_DECRYPT_IV`].
pub const MLTD_TEXT_PBKDF2_HMAC_SHA1_ROUNDS: u32 = 1000;

/// The AES-192-CBC key used to decrypt the text asset.
Expand All @@ -33,9 +43,28 @@ pub const MLTD_TEXT_DECRYPT_IV: &[u8; 16] = &[
0x12, 0x2c, 0x5f, 0xad, 0xcc, 0xa3, 0x68, 0x5d,
];

/// AES-192-CBC encryptor for text assets in MLTD.
pub type MltdTextEncryptor = Encryptor<Aes192>;

/// AES-192-CBC decryptor for text assets in MLTD.
pub type MltdTextDecryptor = Decryptor<Aes192>;

/// Encrypts text using AES-192-CBC with MLTD's key and IV.
///
/// The input text is padded with PKCS7 padding.
///
/// # Errors
///
/// [`Error::Aes`]: if encryption failed.
///
/// # Example
///
/// ```no_run
/// use mltd::extract::text::encrypt_text;
///
/// let text = b"Hello, world!";
/// let cipher = encrypt_text(text).unwrap();
/// ```
pub fn encrypt_text(text: &[u8]) -> Result<Vec<u8>, Error> {
let encryptor =
MltdTextEncryptor::new(MLTD_TEXT_DECRYPT_KEY.into(), MLTD_TEXT_DECRYPT_IV.into());
Expand All @@ -49,6 +78,22 @@ pub fn encrypt_text(text: &[u8]) -> Result<Vec<u8>, Error> {
Ok(buf.to_owned())
}

/// Decrypts text using AES-192-CBC with MLTD's key and IV.
///
/// The output text is unpadded with PKCS7 padding.
///
/// # Errors
///
/// [`Error::Aes`]: if decryption failed.
///
/// # Example
///
/// ```no_run
/// use mltd::extract::text::decrypt_text;
///
/// let cipher = b"Hello, world!";
/// let text = decrypt_text(cipher).unwrap();
/// ```
pub fn decrypt_text(cipher: &[u8]) -> Result<Vec<u8>, Error> {
let decryptor =
MltdTextDecryptor::new(MLTD_TEXT_DECRYPT_KEY.into(), MLTD_TEXT_DECRYPT_IV.into());
Expand Down
1 change: 1 addition & 0 deletions src/mltd/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#![warn(clippy::print_stderr)]
#![warn(clippy::print_stdout)]
#![warn(missing_docs)]

pub mod asset;
mod error;
Expand Down
Loading

0 comments on commit 65a13e0

Please sign in to comment.