diff --git a/src/hactoolnet/CliParser.cs b/src/hactoolnet/CliParser.cs
index 13838592..1ebb1ffb 100644
--- a/src/hactoolnet/CliParser.cs
+++ b/src/hactoolnet/CliParser.cs
@@ -50,6 +50,12 @@ namespace hactoolnet
new CliOption("sign", 0, (o, a) => o.SignSave = true),
new CliOption("title", 1, (o, a) => o.TitleId = ParseTitleId(a[0])),
new CliOption("bench", 1, (o, a) => o.BenchType = a[0]),
+
+ new CliOption("replacefile", 2, (o, a) =>
+ {
+ o.ReplaceFileDest = a[0];
+ o.ReplaceFileSource = a[1];
+ })
};
public static Options Parse(string[] args)
@@ -204,11 +210,12 @@ namespace hactoolnet
sb.AppendLine(" --romfsdir
Specify RomFS directory path. (--title must be specified)");
sb.AppendLine(" --savedir Specify save file directory path.");
sb.AppendLine(" -y, --verify Verify all titles, or verify a single title if --title is set.");
- sb.AppendLine("Savefile options:");
+ sb.AppendLine("Save data options:");
sb.AppendLine(" --outdir Specify directory path to save contents to.");
sb.AppendLine(" --debugoutdir Specify directory path to save intermediate data to for debugging.");
sb.AppendLine(" --sign Sign the save file. (Requires device_key in key file)");
sb.AppendLine(" --listfiles List files in save file.");
+ sb.AppendLine(" --replacefile Replaces a file in the save data");
sb.AppendLine("Keygen options:");
sb.AppendLine(" --outdir Specify directory path to save key files to.");
diff --git a/src/hactoolnet/Options.cs b/src/hactoolnet/Options.cs
index 6efce3d7..b7e3d697 100644
--- a/src/hactoolnet/Options.cs
+++ b/src/hactoolnet/Options.cs
@@ -33,6 +33,8 @@ namespace hactoolnet
public string NormalDir;
public string SecureDir;
public string LogoDir;
+ public string ReplaceFileSource;
+ public string ReplaceFileDest;
public bool ListApps;
public bool ListTitles;
public bool ListNcas;
diff --git a/src/hactoolnet/ProcessSave.cs b/src/hactoolnet/ProcessSave.cs
index af740d2c..1fab9fe9 100644
--- a/src/hactoolnet/ProcessSave.cs
+++ b/src/hactoolnet/ProcessSave.cs
@@ -94,6 +94,39 @@ namespace hactoolnet
duplexDataB.WriteAllBytes(Path.Combine(duplexDir, "DataB"), ctx.Logger);
}
+ if (ctx.Options.ReplaceFileDest != null && ctx.Options.ReplaceFileSource != null)
+ {
+ string destFilename = ctx.Options.ReplaceFileDest;
+ if (!destFilename.StartsWith("/")) destFilename = '/' + destFilename;
+
+ using (IStorage inFile = new FileStream(ctx.Options.ReplaceFileSource, FileMode.Open, FileAccess.Read).AsStorage(false))
+ {
+ using (IStorage outFile = save.OpenFile(destFilename))
+ {
+ if (inFile.Length != outFile.Length)
+ {
+ ctx.Logger.LogMessage($"Replacement file must be the same size as the original file. ({outFile.Length} bytes)");
+ return;
+ }
+
+ inFile.CopyTo(outFile, ctx.Logger);
+
+ ctx.Logger.LogMessage($"Replaced file {destFilename}");
+ }
+ }
+
+ if (save.CommitHeader(ctx.Keyset))
+ {
+ ctx.Logger.LogMessage("Successfully signed save file");
+ }
+ else
+ {
+ ctx.Logger.LogMessage("ERROR: Unable to sign save file. Do you have all the required keys?");
+ }
+
+ return;
+ }
+
if (ctx.Options.SignSave)
{
if (save.CommitHeader(ctx.Keyset))
@@ -104,6 +137,8 @@ namespace hactoolnet
{
ctx.Logger.LogMessage("Unable to sign save file. Do you have all the required keys?");
}
+
+ return;
}
if (ctx.Options.ListFiles)