@@ -37,6 +37,21 @@ pub enum Commands {
3737 input : String ,
3838 /// The directory to extract the WAD to
3939 output : String
40+ } ,
41+ /// Replace existing content in a WAD file with new data
42+ Set {
43+ /// The path to the WAD file to modify
44+ input : String ,
45+ /// The new WAD content
46+ content : String ,
47+ /// An optional output path; defaults to overwriting input WAD file
48+ #[ arg( short, long) ]
49+ output : Option < String > ,
50+ /// An optional new type for the content, can be "Normal", "Shared", or "DLC"
51+ #[ arg( short, long) ]
52+ r#type : Option < String > ,
53+ #[ command( flatten) ]
54+ identifier : ContentIdentifier ,
4055 }
4156}
4257
@@ -55,6 +70,18 @@ pub struct ConvertTargets {
5570 vwii : bool ,
5671}
5772
73+ #[ derive( Args ) ]
74+ #[ clap( next_help_heading = "Content Identifier" ) ]
75+ #[ group( multiple = false , required = true ) ]
76+ pub struct ContentIdentifier {
77+ /// The index of the content to replace
78+ #[ arg( short, long) ]
79+ index : Option < usize > ,
80+ /// The Content ID of the content to replace
81+ #[ arg( short, long) ]
82+ cid : Option < String > ,
83+ }
84+
5885enum Target {
5986 Retail ,
6087 Dev ,
@@ -94,7 +121,7 @@ pub fn convert_wad(input: &str, target: &ConvertTargets, output: &Option<String>
94121 Target :: Vwii => PathBuf :: from ( format ! ( "{}_vWii.wad" , in_path. file_stem( ) . unwrap( ) . to_str( ) . unwrap( ) ) ) ,
95122 }
96123 } ;
97- let mut title = title:: Title :: from_bytes ( fs:: read ( in_path) ?. as_slice ( ) ) . with_context ( || "The provided WAD file could not be parsed, and is likely invalid." ) ?;
124+ let mut title = title:: Title :: from_bytes ( & fs:: read ( in_path) ?) . with_context ( || "The provided WAD file could not be parsed, and is likely invalid." ) ?;
98125 // Bail if the WAD is already using the selected encryption.
99126 if matches ! ( target, Target :: Dev ) && title. ticket . is_dev ( ) {
100127 bail ! ( "This is already a development WAD!" ) ;
@@ -243,3 +270,56 @@ pub fn unpack_wad(input: &str, output: &str) -> Result<()> {
243270 println ! ( "WAD file unpacked!" ) ;
244271 Ok ( ( ) )
245272}
273+
274+ pub fn set_wad ( input : & str , content : & str , output : & Option < String > , identifier : & ContentIdentifier , ctype : & Option < String > ) -> Result < ( ) > {
275+ let in_path = Path :: new ( input) ;
276+ if !in_path. exists ( ) {
277+ bail ! ( "Source WAD \" {}\" could not be found." , in_path. display( ) ) ;
278+ }
279+ let content_path = Path :: new ( content) ;
280+ if !content_path. exists ( ) {
281+ bail ! ( "New content \" {}\" could not be found." , content_path. display( ) ) ;
282+ }
283+ // Get the output name now that we know the target, if one wasn't passed.
284+ let out_path = if output. is_some ( ) {
285+ PathBuf :: from ( output. clone ( ) . unwrap ( ) ) . with_extension ( "wad" )
286+ } else {
287+ in_path. to_path_buf ( )
288+ } ;
289+ // Load the WAD and parse the new type, if one was specified.
290+ let mut title = title:: Title :: from_bytes ( & fs:: read ( in_path) ?) . with_context ( || "The provided WAD file could not be parsed, and is likely invalid." ) ?;
291+ let new_content = fs:: read ( content_path) ?;
292+ let mut target_type: Option < tmd:: ContentType > = None ;
293+ if ctype. is_some ( ) {
294+ target_type = match ctype. clone ( ) . unwrap ( ) . to_ascii_lowercase ( ) . as_str ( ) {
295+ "normal" => Some ( tmd:: ContentType :: Normal ) ,
296+ "shared" => Some ( tmd:: ContentType :: Shared ) ,
297+ "dlc" => Some ( tmd:: ContentType :: DLC ) ,
298+ _ => bail ! ( "The specified content type \" {}\" is invalid!" , ctype. clone( ) . unwrap( ) ) ,
299+ } ;
300+ }
301+ // Parse the identifier passed to choose how to do the find and replace.
302+ if identifier. index . is_some ( ) {
303+ match title. set_content ( & new_content, identifier. index . unwrap ( ) , None , target_type) {
304+ Err ( title:: TitleError :: Content ( content:: ContentError :: IndexOutOfRange { index, max } ) ) => {
305+ bail ! ( "The specified index {} does not exist in this WAD! The maximum index is {}." , index, max)
306+ } ,
307+ Err ( e) => bail ! ( "An unknown error occurred while setting the new content: {e}" ) ,
308+ Ok ( _) => ( ) ,
309+ }
310+ title. fakesign ( ) . with_context ( || "An unknown error occurred while fakesigning the modified WAD." ) ?;
311+ fs:: write ( & out_path, title. to_wad ( ) ?. to_bytes ( ) ?) . with_context ( || "Could not open output file for writing." ) ?;
312+ println ! ( "Successfully replaced content at index {} in WAD file \" {}\" ." , identifier. index. unwrap( ) , out_path. display( ) ) ;
313+ } else if identifier. cid . is_some ( ) {
314+ let cid = u32:: from_str_radix ( identifier. cid . clone ( ) . unwrap ( ) . as_str ( ) , 16 ) . with_context ( || "The specified Content ID is invalid!" ) ?;
315+ let index = match title. content . get_index_from_cid ( cid) {
316+ Ok ( index) => index,
317+ Err ( _) => bail ! ( "The specified Content ID \" {}\" ({}) does not exist in this WAD!" , identifier. cid. clone( ) . unwrap( ) , cid) ,
318+ } ;
319+ title. set_content ( & new_content, index, None , target_type) . with_context ( || "An unknown error occurred while setting the new content." ) ?;
320+ title. fakesign ( ) . with_context ( || "An unknown error occurred while fakesigning the modified WAD." ) ?;
321+ fs:: write ( & out_path, title. to_wad ( ) ?. to_bytes ( ) ?) . with_context ( || "Could not open output file for writing." ) ?;
322+ println ! ( "Successfully replaced content with Content ID \" {}\" ({}) in WAD file \" {}\" ." , identifier. cid. clone( ) . unwrap( ) , cid, out_path. display( ) ) ;
323+ }
324+ Ok ( ( ) )
325+ }
0 commit comments