You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
AM2RModPacker/AM2RModPackerLib/Core.cs

310 lines
12 KiB

using System.Diagnostics;
using System.IO.Compression;
using System.Security;
using System.Security.Cryptography;
using AM2RModPackerLib.XML;
namespace AM2RModPackerLib;
public enum ProfileOperatingSystems
{
Windows,
Linux,
Mac
}
public static class Core
{
public static readonly string Version = "2.0.3";
private const string originalMD5 = "f2b84fe5ba64cb64e284be1066ca08ee";
private static readonly string[] DATAFILES_BLACKLIST = { "data.win", "AM2R.exe", "D3DX9_43.dll", "game.unx" };
// todo: dont do this, cwd is unreliable
private static readonly string localPath = Directory.GetCurrentDirectory();
// TODO: go over thhis and clean
public static (bool, string) CreateModPack(ModProfileXML profile, string input, string originalPath, string apkPath, string output)
{
// Cleanup in case of previous errors
if (Directory.Exists(Path.GetTempPath() + "/AM2RModPacker"))
Directory.Delete(Path.GetTempPath() + "/AM2RModPacker", true);
// Create temp work folders
string tempPath,
tempOriginalPath,
tempModPath,
tempProfilePath;
// We might not have permission to access to the temp directory, so we need to catch the exception.
try
{
tempPath = Directory.CreateDirectory(Path.GetTempPath() + "/AM2RModPacker").FullName;
tempOriginalPath = Directory.CreateDirectory(tempPath + "/original").FullName;
tempModPath = Directory.CreateDirectory(tempPath + "/mod").FullName;
tempProfilePath = Directory.CreateDirectory(tempPath + "/profile").FullName;
}
catch (SecurityException)
{
return (false, "Could not create temp directory! Please run the application with administrator rights.");
}
// Extract 1.1 and modded AM2R to their own directories in temp work
ZipFile.ExtractToDirectory(originalPath, tempOriginalPath);
ZipFile.ExtractToDirectory(input, tempModPath);
if (Directory.Exists(tempModPath + "/AM2R"))
tempModPath += "/AM2R";
// Verify 1.1 with an MD5. If it does not match, exit cleanly and provide a warning window.
try
{
// TODO: dont. do what launcher does
string newMD5 = CalculateMD5(tempOriginalPath + "/data.win");
if (newMD5 != originalMD5)
{
return (false, "1.1 data.win does not meet MD5 checksum! Mod packaging aborted.\n1.1 MD5: " + originalMD5 + "\nYour MD5: " + newMD5);
}
}
catch (FileNotFoundException)
{
return (false, "data.win not found! Are you sure you selected AM2R 1.1? Mod packaging aborted.");
}
// Create AM2R.exe and data.win patches
if (profile.OperatingSystem == "Windows")
{
if (!File.Exists(tempModPath + "/AM2R.exe"))
{ //TODO: put this onto the outer method
/*
var result = MessageBox.Show("Modded game not found, make sure it's not placed in any subfolders.\nCreated profile will likely not be installable, are you sure you want to continue?", "WARNING", MessageBoxButtons.YesNo, MessageBoxType.Warning);
if (result != DialogResult.Yes)
{
AbortPatch();
return (false, "");
}*/
}
if (File.Exists(tempModPath + "profile.xml"))
{
//TODO: put this onto the outer method
/*var result = MessageBox.Show("profile.xml found. This file is used by the AM2RLauncher to determine profile stats and its inclusion may make the profile uninstallable. Are you sure you want to continue?", "WARNING", MessageBoxButtons.YesNo, MessageBoxType.Warning);
if (result != DialogResult.Yes)
{
AbortPatch();
return (false, "");
}*/
}
if (profile.UsesYYC)
{
CreatePatch(tempOriginalPath + "/data.win", tempModPath + "/AM2R.exe", tempProfilePath + "/AM2R.xdelta");
}
else
{
CreatePatch(tempOriginalPath + "/data.win", tempModPath + "/data.win", tempProfilePath + "/data.xdelta");
CreatePatch(tempOriginalPath + "/AM2R.exe", tempModPath + "/AM2R.exe", tempProfilePath + "/AM2R.xdelta");
}
}
else if (profile.OperatingSystem == "Linux")
{
string runnerName = File.Exists(tempModPath + "/" + "AM2R") ? "AM2R" : "runner";
if (!File.Exists(tempModPath + "/" + runnerName))
{//TODO: put this onto the outer method
/*
var result = MessageBox.Show("Modded Linux game not found, make sure it's not placed in any subfolders.\nCreated profile will likely not be installable, are you sure you want to continue?", "WARNING", MessageBoxButtons.YesNo, MessageBoxType.Warning);
if (result != DialogResult.Yes)
{
AbortPatch();
return (false, "");
}*/
}
if (File.Exists(tempModPath + "profile.xml"))
{
//TODO: put this onto the outer method
/* var result = MessageBox.Show("profile.xml found. This file is used by the AM2RLauncher to determine profile stats and its inclusion may make the profile uninstallable. Are you sure you want to continue?", "WARNING", MessageBoxButtons.YesNo, MessageBoxType.Warning);
if (result != DialogResult.Yes)
{
AbortPatch();
return (false, "");
}*/
}
CreatePatch(tempOriginalPath + "/data.win", tempModPath + "/assets/game.unx", tempProfilePath + "/game.xdelta");
CreatePatch(tempOriginalPath + "/AM2R.exe", tempModPath + "/" + runnerName, tempProfilePath + "/AM2R.xdelta");
}
// todo: mac
// Create game.droid patch and wrapper if Android is supported
if (profile.Android)
{
string tempAndroid = Directory.CreateDirectory(tempPath + "/android").FullName;
// Extract APK
// - java -jar apktool.jar d "%~dp0AM2RWrapper_old.apk"
// Process startInfo
// TODO: cross platform
var procStartInfo = new ProcessStartInfo
{
FileName = "cmd.exe",
WorkingDirectory = tempAndroid,
Arguments = "/C java -jar \"" + localPath + "/utilities/android/apktool.jar\" d -f -o \"" + tempAndroid + "\" \"" + apkPath + "\"",
UseShellExecute = false,
CreateNoWindow = true
};
// Run process
using (var proc = new Process { StartInfo = procStartInfo })
{
proc.Start();
proc.WaitForExit();
}
// Create game.droid patch
CreatePatch(tempOriginalPath + "/data.win", tempAndroid + "/assets/game.droid", tempProfilePath + "/droid.xdelta");
// Delete excess files in APK
// Create whitelist
string[] whitelist = { "splash.png", "portrait_splash.png" };
// Get directory
var androidAssets = new DirectoryInfo(tempAndroid + "/assets");
// Delete files
foreach (var file in androidAssets.GetFiles())
{
if (file.Name.EndsWith(".ini") && file.Name != "modifiers.ini")
{
if (File.Exists(tempProfilePath + "/AM2R.ini"))
// This shouldn't be a problem... normally...
File.Delete(tempProfilePath + "/AM2R.ini");
File.Copy(file.FullName, tempProfilePath + "/AM2R.ini");
}
if (!whitelist.Contains(file.Name))
File.Delete(file.FullName);
}
foreach (var dir in androidAssets.GetDirectories())
Directory.Delete(dir.FullName, true);
// Create wrapper
// Process startInfo
// - java -jar apktool.jar b "%~dp0AM2RWrapper_old" -o "%~dp0AM2RWrapper.apk"
var procStartInfo2 = new ProcessStartInfo
{
FileName = "cmd.exe",
WorkingDirectory = tempAndroid,
Arguments = "/C java -jar \"" + localPath + "/utilities/android/apktool.jar\" b -f \"" + tempAndroid + "\" -o \"" + tempProfilePath + "/AM2RWrapper.apk\"",
UseShellExecute = false,
CreateNoWindow = true
};
// Run process
using (var proc = new Process { StartInfo = procStartInfo2 })
{
proc.Start();
proc.WaitForExit();
}
string tempAndroidProfilePath = tempProfilePath + "/android";
Directory.CreateDirectory(tempAndroidProfilePath);
File.Move(tempProfilePath + "/AM2RWrapper.apk", tempAndroidProfilePath + "/AM2RWrapper.apk");
if (File.Exists(tempProfilePath + "/AM2R.ini"))
File.Move(tempProfilePath + "/AM2R.ini", tempAndroidProfilePath + "/AM2R.ini");
}
// Copy datafiles (exclude .ogg if custom music is not selected)
var dirInfo = new DirectoryInfo(tempModPath);
if (profile.OperatingSystem == "Linux")
dirInfo = new DirectoryInfo(tempModPath + "/assets");
Directory.CreateDirectory(tempProfilePath + "/files_to_copy");
if (profile.UsesCustomMusic)
{
// Copy files, excluding the blacklist
CopyFilesRecursive(dirInfo, DATAFILES_BLACKLIST, tempProfilePath + "/files_to_copy");
}
else
{
// Get list of 1.1's music files
string[] musFiles = Directory.GetFiles(tempOriginalPath, "*.ogg").Select(file => Path.GetFileName(file)).ToArray();
if (profile.OperatingSystem == "Linux")
musFiles = Directory.GetFiles(tempOriginalPath, "*.ogg").Select(file => Path.GetFileName(file).ToLower()).ToArray();
// Combine musFiles with the known datafiles for a blacklist
string[] blacklist = musFiles.Concat(DATAFILES_BLACKLIST).ToArray();
// Copy files, excluding the blacklist
CopyFilesRecursive(dirInfo, blacklist, tempProfilePath + "/files_to_copy");
}
// Export profile as XML
string xmlOutput = Serializer.Serialize<ModProfileXML>(profile);
File.WriteAllText(tempProfilePath + "/profile.xml", xmlOutput);
// Compress temp folder to .zip
if (File.Exists(output))
File.Delete(output);
ZipFile.CreateFromDirectory(tempProfilePath, output);
// Delete temp folder
Directory.Delete(tempPath, true);
return (true, "");
}
public static void CreatePatch(string original, string modified, string output)
{
// Specify process start info
var parameters = new ProcessStartInfo
{
// TODO: deal with linux/mac
FileName = localPath + "/utilities/xdelta/xdelta3.exe",
WorkingDirectory = localPath,
UseShellExecute = false,
CreateNoWindow = true,
Arguments = "-f -e -s \"" + original + "\" \"" + modified + "\" \"" + output + "\""
};
// Launch process and wait for exit. using statement automatically disposes the object for us!
using var proc = new Process { StartInfo = parameters };
proc.Start();
proc.WaitForExit();
}
public static string CalculateMD5(string filename)
{
using var stream = File.OpenRead(filename);
using var md5 = MD5.Create();
byte[] hash = md5.ComputeHash(stream);
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
}
public static void CopyFilesRecursive(DirectoryInfo source, string[] blacklist, string destination)
{
foreach (var file in source.GetFiles())
{
if (!blacklist.Contains(file.Name))
file.CopyTo(destination + "/" + file.Name);
}
foreach (var dir in source.GetDirectories())
{
// Folders need to be lowercase, because GM only reads from lowercase names on *nix systems. Windows is case-insensitive so doesnt matter for them
string newDir = Directory.CreateDirectory(destination + "/" + dir.Name.ToLower()).FullName;
CopyFilesRecursive(dir, blacklist, newDir);
}
}
}