diff --git a/AM2RPortHelperCLI/Program.cs b/AM2RPortHelperCLI/Program.cs index 14a8082..9efaea2 100644 --- a/AM2RPortHelperCLI/Program.cs +++ b/AM2RPortHelperCLI/Program.cs @@ -8,12 +8,13 @@ namespace AM2RPortHelper; internal static class Program { - //TODO: add "-l" flag. port launcher mods to different versions. - +// TODO: implement launcher flag -u launcher private static int Main(string[] args) { Console.WriteLine("AM2RPortHelperCLI v" + PortHelper.Version); + PortHelper.PortLauncherMod("/home/narr/Downloads/Multitroid1_4_2VM_Linux.zip", "Windows", false, "./foo.zip"); + var interactiveOption = new Option(new[] { "-i", "--interactive" }, "Use an interactive mode. This will ignore all other options."); var fileOption = new Option(new[] { "-f", "--file" }, "The file path to the raw mod that should be ported. *REQUIRED*"); var linuxOption = new Option(new[] { "-l", "--linux" }, "The output file path for the Linux mod. None given equals to no Linux port."); diff --git a/AM2RPortHelperGUI/AM2RPortHelperGUI/MainForm.cs b/AM2RPortHelperGUI/AM2RPortHelperGUI/MainForm.cs index 891670a..b755245 100644 --- a/AM2RPortHelperGUI/AM2RPortHelperGUI/MainForm.cs +++ b/AM2RPortHelperGUI/AM2RPortHelperGUI/MainForm.cs @@ -39,6 +39,7 @@ public partial class MainForm : Form Content = mainLayout }; + // TODO: think of a way to present this normally var am2rModLayout = new DynamicLayout(); var am2rModPage = new TabPage { diff --git a/AM2RPortHelperLib/HelperMethods.cs b/AM2RPortHelperLib/HelperMethods.cs new file mode 100644 index 0000000..c82f422 --- /dev/null +++ b/AM2RPortHelperLib/HelperMethods.cs @@ -0,0 +1,77 @@ +using System.Security.Cryptography; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; + +namespace AM2RPortHelperLib; + +public static partial class PortHelper +{ + private static void LowercaseFolder(string directory) + { + DirectoryInfo dir = new DirectoryInfo(directory); + + foreach(var file in dir.GetFiles()) + { + if (file.Name == file.Name.ToLower()) continue; + file.MoveTo(file.DirectoryName + "/" + file.Name.ToLower()); + } + + foreach(var subDir in dir.GetDirectories()) + { + if (subDir.Name == subDir.Name.ToLower()) continue; + subDir.MoveTo(subDir.Parent.FullName + "/" + subDir.Name.ToLower()); + LowercaseFolder(subDir.FullName); + } + } + + private static void DirectoryCopy(string sourceDirName, string destDirName, bool copySubDirs) + { + // Get the subdirectories for the specified directory. + DirectoryInfo dir = new DirectoryInfo(sourceDirName); + + if (!dir.Exists) + throw new DirectoryNotFoundException($"Source directory does not exist or could not be found: {sourceDirName}"); + + DirectoryInfo[] dirs = dir.GetDirectories(); + + // If the destination directory doesn't exist, create it. + Directory.CreateDirectory(destDirName); + + // Get the files in the directory and copy them to the new location. + FileInfo[] files = dir.GetFiles(); + foreach (FileInfo file in files) + { + string tempPath = Path.Combine(destDirName, file.Name); + file.CopyTo(tempPath, true); + } + + if (!copySubDirs) + return; + + // If copying subdirectories, copy them and their contents to new location. + foreach (DirectoryInfo subDir in dirs) + { + string tempPath = Path.Combine(destDirName, subDir.Name); + DirectoryCopy(subDir.FullName, tempPath, true); + } + } + + private static void SaveAndroidIcon(Image icon, int dimensions, string filePath) + { + Image picture = icon; + picture.Mutate(x => x.Resize(dimensions, dimensions, KnownResamplers.NearestNeighbor)); + picture.SaveAsPng(filePath); + } + + private static string CalculateSHA256(string filename) + { + // Check if file exists first + if (!File.Exists(filename)) + return ""; + + using FileStream stream = File.OpenRead(filename); + using SHA256 sha256 = SHA256.Create(); + byte[] hash = sha256.ComputeHash(stream); + return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); + } +} \ No newline at end of file diff --git a/AM2RPortHelperLib/PortHelper.cs b/AM2RPortHelperLib/PortHelper.cs index a57a62f..429cb5f 100644 --- a/AM2RPortHelperLib/PortHelper.cs +++ b/AM2RPortHelperLib/PortHelper.cs @@ -2,17 +2,16 @@ using System.IO.Compression; using System.Runtime.InteropServices; using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Processing; namespace AM2RPortHelperLib; -public static class PortHelper +public static partial class PortHelper { public const string Version = "1.3"; public delegate void OutputHandlerDelegate(string output); private static OutputHandlerDelegate outputHandler; - + private static void SendOutput(string output) { if (outputHandler is not null) @@ -20,11 +19,135 @@ public static class PortHelper else Console.WriteLine(output); } - + private static readonly string tmp = Path.GetTempPath(); private static readonly string currentDir = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory); private static readonly string utilDir = currentDir + "/utils"; + + public static void PortLauncherMod(string inputLauncherZipPath, string targetOS, bool includeAndroid, string outputLauncherZipPath, OutputHandlerDelegate outputDelegate = null) + { + outputHandler = outputDelegate; + string extractDirectory = tmp + "/" + Path.GetFileNameWithoutExtension(inputLauncherZipPath); + string filesToCopyDir = extractDirectory + "/files_to_copy"; + + // Check if temp folder exists, delete if yes, extract zip to there + if (Directory.Exists(extractDirectory)) + Directory.Delete(extractDirectory, true); + SendOutput("Extracting Launcher mod..."); + ZipFile.ExtractToDirectory(inputLauncherZipPath, extractDirectory); + + // Run sha256 hash on runner to see if it's supported! + string runnerHash = CalculateSHA256(extractDirectory + "/AM2R.xdelta"); + string[] allowedHashes = new[] { "b78c4fd2dc481f97b60440a5c89786da284b4aaeeba9fb2e3b48ac369cfe50d5", "243509f4270f448411c8405b71d7bc4f5d4fe5f3ecc1638d9c1218bf76b69f1f", "852b9a9466f99a53260b8147c6d286b81c145b2c10b00bb5c392b40b035811b5"}; + if (!allowedHashes.Contains(runnerHash)) + throw new NotSupportedException("Invalid GM:S version! Porting Launcher mods is only supported for mods build with GM:S 1.4.1763!"); + + var profile = Serializer.Deserialize(File.ReadAllText(extractDirectory + "/profile.xml")); + if (profile.UsesYYC) + throw new NotSupportedException("Launcher Modis YYC, cannot port!"); + string currentOS = profile.OperatingSystem; + bool isAndroidIncluded = profile.SupportsAndroid; + + if (targetOS == profile.OperatingSystem) + { + SendOutput("Target OS and Launcher OS are the same; exiting."); + return; + } + + switch (targetOS) + { + case "Windows": + { + File.Move(extractDirectory + "/game.xdelta", extractDirectory + "/data.xdelta"); + + // get proper runner + File.Delete(extractDirectory + "/AM2R.xdelta"); + File.Copy(utilDir + "/windowsRunner.xdelta", extractDirectory + "/AM2R.xdelta"); + + // Windows doesn't care about capitalization and because I can't predict how it originally was, I'm going to ignore it. + + // Windows doesn't have icons/splashes, so we remove those if they exist. + if (!File.Exists(filesToCopyDir + "/icon.png")) + File.Delete(filesToCopyDir + "/icon.png"); + if (!File.Exists(filesToCopyDir + "/splash.png")) + File.Delete(filesToCopyDir + "/splash.png"); + + // Properly set profile.xml variables. + profile.OperatingSystem = "Windows"; + profile.SaveLocation = currentOS switch + { + "Linux" => profile.SaveLocation.Replace("~/.config", "%localappdata%"), + "Mac" => profile.SaveLocation.Replace("~/Library/Application Support", "%localappdata%"), + _ => throw new NotSupportedException("Unsupported OS " + currentOS) + }; + File.WriteAllText(extractDirectory + "/profile.xml",Serializer.Serialize(profile)); + break; + } + + case "Linux": + { + if (currentOS == "Windows") + File.Move(extractDirectory + "/data.xdelta", extractDirectory + "/game.xdelta"); + + // get proper runner + File.Delete(extractDirectory + "/AM2R.xdelta"); + File.Copy(utilDir + "/linuxRunner.xdelta", extractDirectory + "/AM2R.xdelta"); + + // Linux needs everything lowercased. Only needed if we're coming from Windows + if (currentOS == "Windows") + LowercaseFolder(extractDirectory + "/files_to_copy"); + + // Windows doesn't have icon/splash, so we copy them over from here + if (!File.Exists(filesToCopyDir + "/icon.png")) + File.Copy(utilDir + "/icon.png", filesToCopyDir + "/icon.png"); + if (!File.Exists(filesToCopyDir + "/splash.png")) + File.Copy(utilDir + "/splash.png", filesToCopyDir + "/splash.png"); + + // Properly set profile.xml variables + profile.OperatingSystem = "Linux"; + profile.SaveLocation = currentOS switch + { + "Windows" => profile.SaveLocation.Replace("%localappdata%", "~/.config"), + "Mac" => profile.SaveLocation.Replace("~/Library/Application Support", "~/.config"), + _ => throw new NotSupportedException("Unsupported OS " + currentOS) + }; + File.WriteAllText(extractDirectory + "/profile.xml",Serializer.Serialize(profile)); + break; + } + + case "Mac": + { + //TODO: write this Somewhat difficult, since it's not just a data swap, we need to run UMT on it as well to transfer the BC version n stuff + + break; + } + } + + if (!includeAndroid) + { + if (File.Exists(extractDirectory + "/droid.xdelta")) + File.Delete(extractDirectory + "/droid.xdelta"); + if (Directory.Exists(extractDirectory + "/android")) + Directory.Delete(extractDirectory + "/android", true); + } + else + { + // If APK is not there, we need to create the APK ourselves. + if (!isAndroidIncluded) + { + //TODO: see above + } + } + + //zip the result + SendOutput($"Creating Launcher zip for {targetOS}..."); + ZipFile.CreateFromDirectory(extractDirectory, outputLauncherZipPath); + + // Clean up + Directory.Delete(extractDirectory, true); + } + // TODO: Make these not windows -> OS, but Raw -> OS public static void PortWindowsToLinux(string inputRawZipPath, string outputRawZipPath, OutputHandlerDelegate outputDelegate = null) { outputHandler = outputDelegate; @@ -63,7 +186,7 @@ public static class PortHelper //recursively lowercase everything in the assets folder LowercaseFolder(assetsDir); - //zip the result if no + //zip the result SendOutput("Creating Linux zip..."); ZipFile.CreateFromDirectory(extractDirectory, outputRawZipPath); @@ -161,7 +284,7 @@ public static class PortHelper p.Start(); p.WaitForExit(); - //Move apk if it doesn't exist already + //Move apk File.Move(finalApkBuild, outputRawApkPath); // Clean up @@ -258,7 +381,7 @@ public static class PortHelper Directory.Delete(extractDirectory, true); - //zip the result if no + //zip the result SendOutput("Creating Mac zip..."); ZipFile.CreateFromDirectory(baseTempDirectory, outputRawZipPath); @@ -266,60 +389,5 @@ public static class PortHelper Directory.Delete(baseTempDirectory, true); } - private static void LowercaseFolder(string directory) - { - DirectoryInfo dir = new DirectoryInfo(directory); - - foreach(var file in dir.GetFiles()) - { - if (file.Name == file.Name.ToLower()) continue; - file.MoveTo(file.DirectoryName + "/" + file.Name.ToLower()); - } - - foreach(var subDir in dir.GetDirectories()) - { - if (subDir.Name == subDir.Name.ToLower()) continue; - subDir.MoveTo(subDir.Parent.FullName + "/" + subDir.Name.ToLower()); - LowercaseFolder(subDir.FullName); - } - } - - private static void DirectoryCopy(string sourceDirName, string destDirName, bool copySubDirs) - { - // Get the subdirectories for the specified directory. - DirectoryInfo dir = new DirectoryInfo(sourceDirName); - - if (!dir.Exists) - throw new DirectoryNotFoundException($"Source directory does not exist or could not be found: {sourceDirName}"); - - DirectoryInfo[] dirs = dir.GetDirectories(); - - // If the destination directory doesn't exist, create it. - Directory.CreateDirectory(destDirName); - - // Get the files in the directory and copy them to the new location. - FileInfo[] files = dir.GetFiles(); - foreach (FileInfo file in files) - { - string tempPath = Path.Combine(destDirName, file.Name); - file.CopyTo(tempPath, true); - } - - if (!copySubDirs) - return; - - // If copying subdirectories, copy them and their contents to new location. - foreach (DirectoryInfo subDir in dirs) - { - string tempPath = Path.Combine(destDirName, subDir.Name); - DirectoryCopy(subDir.FullName, tempPath, true); - } - } - private static void SaveAndroidIcon(Image icon, int dimensions, string filePath) - { - Image picture = icon; - picture.Mutate(x => x.Resize(dimensions, dimensions, KnownResamplers.NearestNeighbor)); - picture.SaveAsPng(filePath); - } } \ No newline at end of file diff --git a/AM2RPortHelperLib/ProfileXML.cs b/AM2RPortHelperLib/ProfileXML.cs new file mode 100644 index 0000000..f9866b8 --- /dev/null +++ b/AM2RPortHelperLib/ProfileXML.cs @@ -0,0 +1,97 @@ +using System.Xml.Serialization; + +namespace AM2RPortHelperLib; + +/// +/// Class that handles how the mod settings are saved as XML. +/// +[Serializable] +[XmlRoot("message")] +public class ProfileXML +{ + /// Indicates the Operating system the mod was made for. + [XmlAttribute("OperatingSystem")] + public string OperatingSystem + { get; set; } + /// Indicates the xml version the mod was made in. + [XmlAttribute("XMLVersion")] + public int XMLVersion + { get; set; } + /// Indicates the version of the mod. + [XmlAttribute("Version")] + public string Version + { get; set; } + /// Indicates the mod's name. + [XmlAttribute("Name")] + public string Name + { get; set; } + /// Indicates the mod's author. + [XmlAttribute("Author")] + public string Author + { get; set; } + /// Indicates whether or not the mod uses custom music. + [XmlAttribute("UsesCustomMusic")] + public bool UsesCustomMusic + { get; set; } + /// Indicates the save location of the mod. + [XmlAttribute("SaveLocation")] + public string SaveLocation + { get; set; } + /// Indicates whether or not the mod supports Android. + [XmlAttribute("SupportsAndroid")] + public bool SupportsAndroid + { get; set; } + /// Indicates whether or not the mod was compiled with YYC. + [XmlAttribute("UsesYYC")] + public bool UsesYYC + { get; set; } + /// Indicates if the mod is installable. This is only for archival community updates mods. + [XmlAttribute("Installable")] + public bool Installable + { get; set; } + /// Indicates any notes that the mod author deemed worthy to share about his mod. + [XmlAttribute("ProfileNotes")] + public string ProfileNotes + { get; set; } + /// This gets calculated at runtime, by the Launcher. Indicates where the install data for the mod is stored. + [XmlIgnore] + public string DataPath + { get; set; } + + /// Creates a with a default set of attributes. + public ProfileXML() + { + Installable = true; + } + + /// + /// Creates a with a custom set of attributes. + /// + /// The operating system the mod was made on. + /// The xml version the mod was created with. + /// The version of the mod. + /// The mod name. + /// The mod author. + /// Whether or not the mod uses custom music. + /// The save location of the mod. + /// Whether or not the mod works for android. + /// Whether or not the mod was made with YYC. + /// Whether or not the mod is installable. + /// The notes of the mod. + public ProfileXML(string operatingSystem, int xmlVersion, string version, string name, string author, + bool usesCustomMusic, string saveLocation, bool android, bool usesYYC, + bool installable, string profileNotes) + { + OperatingSystem = operatingSystem; + XMLVersion = xmlVersion; + Version = version; + Name = name; + Author = author; + UsesCustomMusic = usesCustomMusic; + SaveLocation = saveLocation; + SupportsAndroid = android; + UsesYYC = usesYYC; + Installable = installable; + ProfileNotes = profileNotes; + } +} \ No newline at end of file diff --git a/AM2RPortHelperLib/Serializer.cs b/AM2RPortHelperLib/Serializer.cs new file mode 100644 index 0000000..4aa0d9a --- /dev/null +++ b/AM2RPortHelperLib/Serializer.cs @@ -0,0 +1,47 @@ +using System.Text; + +namespace AM2RPortHelperLib; + +/// +/// The Serializer class, that serializes to and deserializes from XML files. +/// +public static class Serializer +{ + /// + /// Serializes as a to XML. + /// + /// The class to serialize to. + /// The object that will be serialized. + /// The serialized XML as a . + public static string Serialize(object item) + { + Type t = typeof(T); + MemoryStream memStream = new MemoryStream(); + System.Xml.Serialization.XmlSerializer serializer = new System.Xml.Serialization.XmlSerializer(t); + + serializer.Serialize(memStream, item); + + string xml = Encoding.UTF8.GetString(memStream.ToArray()); + + memStream.Flush(); + memStream.Close(); + memStream.Dispose(); + memStream = null; + + return xml; + } + + /// + /// Deserialize into an object of class that can be assigned. + /// + /// The class that will be deserialized to. + /// An XML that will be deserialized. + /// A deserialized object of class from . + public static T Deserialize(string xmlString) + { + Type t = typeof(T); + using MemoryStream memStream = new MemoryStream(Encoding.UTF8.GetBytes(xmlString)); + System.Xml.Serialization.XmlSerializer serializer = new System.Xml.Serialization.XmlSerializer(t); + return (T)serializer.Deserialize(memStream); + } +} \ No newline at end of file diff --git a/AM2RPortHelperLib/utils/linuxRunner.xdelta b/AM2RPortHelperLib/utils/linuxRunner.xdelta new file mode 100644 index 0000000..f92f911 Binary files /dev/null and b/AM2RPortHelperLib/utils/linuxRunner.xdelta differ diff --git a/AM2RPortHelperLib/utils/macRunner.xdelta b/AM2RPortHelperLib/utils/macRunner.xdelta new file mode 100644 index 0000000..51e81d6 Binary files /dev/null and b/AM2RPortHelperLib/utils/macRunner.xdelta differ diff --git a/AM2RPortHelperLib/utils/windowsRunner.xdelta b/AM2RPortHelperLib/utils/windowsRunner.xdelta new file mode 100644 index 0000000..3c11eda Binary files /dev/null and b/AM2RPortHelperLib/utils/windowsRunner.xdelta differ