Initial port launcher mods in library

pull/5/head
Miepee 4 years ago
parent 76d9cca3c9
commit 3e235045e1

@ -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<bool>(new[] { "-i", "--interactive" }, "Use an interactive mode. This will ignore all other options.");
var fileOption = new Option<FileInfo>(new[] { "-f", "--file" }, "The file path to the raw mod that should be ported. *REQUIRED*");
var linuxOption = new Option<FileInfo>(new[] { "-l", "--linux" }, "The output file path for the Linux mod. None given equals to no Linux port.");

@ -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
{

@ -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();
}
}

@ -2,11 +2,10 @@
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);
@ -25,6 +24,130 @@ public static class PortHelper
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<ProfileXML>(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<ProfileXML>(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<ProfileXML>(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);
}
}

@ -0,0 +1,97 @@
using System.Xml.Serialization;
namespace AM2RPortHelperLib;
/// <summary>
/// Class that handles how the mod settings are saved as XML.
/// </summary>
[Serializable]
[XmlRoot("message")]
public class ProfileXML
{
/// <summary>Indicates the Operating system the mod was made for.</summary>
[XmlAttribute("OperatingSystem")]
public string OperatingSystem
{ get; set; }
/// <summary>Indicates the xml version the mod was made in.</summary>
[XmlAttribute("XMLVersion")]
public int XMLVersion
{ get; set; }
/// <summary>Indicates the version of the mod.</summary>
[XmlAttribute("Version")]
public string Version
{ get; set; }
/// <summary>Indicates the mod's name.</summary>
[XmlAttribute("Name")]
public string Name
{ get; set; }
/// <summary>Indicates the mod's author.</summary>
[XmlAttribute("Author")]
public string Author
{ get; set; }
/// <summary>Indicates whether or not the mod uses custom music.</summary>
[XmlAttribute("UsesCustomMusic")]
public bool UsesCustomMusic
{ get; set; }
/// <summary>Indicates the save location of the mod.</summary>
[XmlAttribute("SaveLocation")]
public string SaveLocation
{ get; set; }
/// <summary>Indicates whether or not the mod supports Android.</summary>
[XmlAttribute("SupportsAndroid")]
public bool SupportsAndroid
{ get; set; }
/// <summary>Indicates whether or not the mod was compiled with YYC.</summary>
[XmlAttribute("UsesYYC")]
public bool UsesYYC
{ get; set; }
/// <summary>Indicates if the mod is installable. This is only <see langword="false"/> for archival community updates mods.</summary>
[XmlAttribute("Installable")]
public bool Installable
{ get; set; }
/// <summary>Indicates any notes that the mod author deemed worthy to share about his mod.</summary>
[XmlAttribute("ProfileNotes")]
public string ProfileNotes
{ get; set; }
/// <summary>This gets calculated at runtime, by the Launcher. Indicates where the install data for the mod is stored.</summary>
[XmlIgnore]
public string DataPath
{ get; set; }
/// <summary>Creates a <see cref="ProfileXML"/> with a default set of attributes.</summary>
public ProfileXML()
{
Installable = true;
}
/// <summary>
/// Creates a <see cref="ProfileXML"/> with a custom set of attributes.
/// </summary>
/// <param name="operatingSystem">The operating system the mod was made on.</param>
/// <param name="xmlVersion">The xml version the mod was created with.</param>
/// <param name="version">The version of the mod.</param>
/// <param name="name">The mod name.</param>
/// <param name="author">The mod author.</param>
/// <param name="usesCustomMusic">Whether or not the mod uses custom music.</param>
/// <param name="saveLocation">The save location of the mod.</param>
/// <param name="android">Whether or not the mod works for android.</param>
/// <param name="usesYYC">Whether or not the mod was made with YYC.</param>
/// <param name="installable">Whether or not the mod is installable.</param>
/// <param name="profileNotes">The notes of the mod.</param>
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;
}
}

@ -0,0 +1,47 @@
using System.Text;
namespace AM2RPortHelperLib;
/// <summary>
/// The <c>Serializer</c> class, that serializes to and deserializes from XML files.
/// </summary>
public static class Serializer
{
/// <summary>
/// Serializes <paramref name="item"/> as a <typeparamref name="T"/> to XML.
/// </summary>
/// <typeparam name="T">The class to serialize to.</typeparam>
/// <param name="item">The object that will be serialized.</param>
/// <returns>The serialized XML as a <see cref="string"/>.</returns>
public static string Serialize<T>(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;
}
/// <summary>
/// Deserialize <paramref name="xmlString"/> into an object of class <typeparamref name="T"/> that can be assigned.
/// </summary>
/// <typeparam name="T">The class that <paramref name="xmlString"/> will be deserialized to.</typeparam>
/// <param name="xmlString">An XML <see cref="string"/> that will be deserialized.</param>
/// <returns>A deserialized object of class <typeparamref name="T"/> from <paramref name="xmlString"/>.</returns>
public static T Deserialize<T>(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);
}
}
Loading…
Cancel
Save