@ -1,199 +1,15 @@
using System.Diagnostics ;
using System.Diagnostics ;
using System.IO.Compression ;
using System.IO.Compression ;
using System.Runtime.CompilerServices ;
using System.Runtime.InteropServices ;
using System.Runtime.InteropServices ;
using SixLabors.ImageSharp ;
using SixLabors.ImageSharp.Processing ;
using SixLabors.ImageSharp.Processing.Processors.Transforms ;
namespace AM2RPortHelperLib ;
namespace AM2RPortHelperLib ;
public enum LauncherModTarget s
public abstract class RawMods : IMod s
{
{
Windows ,
Linux ,
Mac
}
public static partial class PortHelper
{
/// <summary>
/// The current version of <see cref="AM2RPortHelperLib"/>.
/// </summary>
public const string Version = "1.4" ;
public delegate void OutputHandlerDelegate ( string output ) ;
private static OutputHandlerDelegate outputHandler ;
private static void SendOutput ( string output )
{
outputHandler ? . Invoke ( output ) ;
}
/// <summary>
/// A temporary directory
/// </summary>
private static readonly string tmp = Path . GetTempPath ( ) ;
/// <summary>
/// The current directory of the AM2RPortHelper program.
/// </summary>
private static readonly string currentDir = Path . GetDirectoryName ( AppDomain . CurrentDomain . BaseDirectory ) ;
/// <summary>
/// The "utils" folder that's shipped with the AM2RPortHelper.
/// </summary>
private static readonly string utilDir = currentDir + "/utils" ;
/// <summary>
/// Ports a Mod zip intended to be installed via the AM2RLauncher to other operating systems.
/// </summary>
/// <param name="inputLauncherZipPath">The path to the AM2RLauncher mod zip that should be ported.</param>
/// <param name="targetOS">The target operating system to port the </param>
/// <param name="includeAndroid">Whether Android should be inlcuded in the port.</param>
/// <param name="outputLauncherZipPath">The path where the ported AM2RLauncher mod zip should be saved.</param>
/// <param name="am2r11ZipPath">The path to an AM2R 1.1 zip path. This is *required* if the input launcher zip is for Mac and will be ignored if the input zip is for anything else.</param>
/// <param name="outputDelegate">The function that should handle in-progress output messages.</param>
/// <exception cref="NotSupportedException">WIP</exception>
/// TODO: other exceptions
public static void PortLauncherMod ( string inputLauncherZipPath , LauncherModTargets targetOS , bool includeAndroid , string outputLauncherZipPath , string am2r11ZipPath = null , 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 ) ;
var profile = Serializer . Deserialize < ProfileXML > ( File . ReadAllText ( extractDirectory + "/profile.xml" ) ) ;
if ( profile . UsesYYC )
throw new NotSupportedException ( "Launcher Mod is YYC, cannot port!" ) ;
string currentOS = profile . OperatingSystem ;
bool isAndroidIncluded = profile . SupportsAndroid ;
if ( targetOS . ToString ( ) = = profile . OperatingSystem )
{
SendOutput ( "Target OS and Launcher OS are the same; exiting." ) ;
return ;
}
// Run sha256 hash on runner to see if it's supported!
string runnerHash = CalculateSHA256 ( extractDirectory + "/AM2R.xdelta" ) ;
string [ ] allowedHashes = new [ ] { "b78c4fd2dc481f97b60440a5c89786da284b4aaeeba9fb2e3b48ac369cfe50d5" , "243509f4270f448411c8405b71d7bc4f5d4fe5f3ecc1638d9c1218bf76b69f1f" , "852b9a9466f99a53260b8147c6d286b81c145b2c10b00bb5c392b40b035811b5" } ;
// Don't check has on Windows, because the runner there has icons embedded in it, screwing off the hashes
// TODO: find a way around that
if ( ! ( profile . OperatingSystem = = "Windows" | | 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!" ) ;
// TODO: Not sure if this is ever gonna be possible, since it requires one to shift back the patch.
// We'd need a 1.1 file to apply the patch to, run that with umtlib to shift it back, and then apply a new patch.
if ( profile . OperatingSystem = = "Mac" )
throw new NotSupportedException ( "Porting Mac mods is currently not supported!" ) ;
switch ( targetOS )
{
case LauncherModTargets . Windows :
{
// We have a non-windows launcher mod, where data file patch is guaranteed to be game.xdelta
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 LauncherModTargets . 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 LauncherModTargets . Mac :
{
// TODO: Not sure if this is ever gonna be possible, since it requires one to shift up the patch.
// We'd need a 1.1 file to apply the patch to, run that with umtlib to shift it up, and then apply a new patch.
throw new NotSupportedException ( "Porting Mac mods is currently not supported!" ) ;
}
default : throw new ArgumentOutOfRangeException ( nameof ( targetOS ) , targetOS , "Unknown target to port to!" ) ;
}
if ( ! includeAndroid )
{
if ( File . Exists ( extractDirectory + "/droid.xdelta" ) )
File . Delete ( extractDirectory + "/droid.xdelta" ) ;
if ( Directory . Exists ( extractDirectory + "/android" ) )
Directory . Delete ( extractDirectory + "/android" , true ) ;
profile . SupportsAndroid = false ;
}
else
{
// If APK is not there, we need to create the APK ourselves.
if ( ! isAndroidIncluded )
{
//TODO: see above
}
profile . SupportsAndroid = true ;
}
//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
// TODO: Make these not windows -> OS, but Raw -> OS
public static void Port Windows ToLinux( string inputRawZipPath , string outputRawZipPath , OutputHandlerDelegate outputDelegate = null )
public static void PortToLinux ( string inputRawZipPath , string outputRawZipPath , OutputHandlerDelegate outputDelegate = null )
{
{
outputHandler = outputDelegate ;
outputHandler = outputDelegate ;
string extractDirectory = tmp + "/" + Path . GetFileNameWithoutExtension ( inputRawZipPath ) ;
string extractDirectory = tmp + "/" + Path . GetFileNameWithoutExtension ( inputRawZipPath ) ;
string assetsDir = extractDirectory + "/assets" ;
string assetsDir = extractDirectory + "/assets" ;
@ -228,7 +44,7 @@ public static partial class PortHelper
File . Copy ( utilDir + "/splash.png" , assetsDir + "/splash.png" ) ;
File . Copy ( utilDir + "/splash.png" , assetsDir + "/splash.png" ) ;
//recursively lowercase everything in the assets folder
//recursively lowercase everything in the assets folder
LowercaseFolder( assetsDir ) ;
HelperMethods. LowercaseFolder( assetsDir ) ;
//zip the result
//zip the result
SendOutput ( "Creating Linux zip..." ) ;
SendOutput ( "Creating Linux zip..." ) ;
@ -239,7 +55,7 @@ public static partial class PortHelper
}
}
// TODO: try to figure out if its possible to extract the name from the data.win file and then just offer a "use custom save directory" option that decides whether to use it or not.
// TODO: try to figure out if its possible to extract the name from the data.win file and then just offer a "use custom save directory" option that decides whether to use it or not.
public static void Port Windows ToAndroid( string inputRawZipPath , string outputRawApkPath , string modName = null , bool usesInternet = false , OutputHandlerDelegate outputDelegate = null )
public static void Port ToAndroid( string inputRawZipPath , string outputRawApkPath , string modName = null , bool usesInternet = false , OutputHandlerDelegate outputDelegate = null )
{
{
outputHandler = outputDelegate ;
outputHandler = outputDelegate ;
string extractDirectory = tmp + "/" + Path . GetFileNameWithoutExtension ( inputRawZipPath ) ;
string extractDirectory = tmp + "/" + Path . GetFileNameWithoutExtension ( inputRawZipPath ) ;
@ -287,7 +103,7 @@ public static partial class PortHelper
File . Copy ( utilDir + "/splashAndroid.png" , apkAssetsDir + "/splash.png" , true ) ;
File . Copy ( utilDir + "/splashAndroid.png" , apkAssetsDir + "/splash.png" , true ) ;
//recursively lowercase everything in the assets folder
//recursively lowercase everything in the assets folder
LowercaseFolder( apkAssetsDir ) ;
HelperMethods. LowercaseFolder( apkAssetsDir ) ;
// Edit apktool.yml to not compress music
// Edit apktool.yml to not compress music
string yamlFile = File . ReadAllText ( apkDir + "/apktool.yml" ) ;
string yamlFile = File . ReadAllText ( apkDir + "/apktool.yml" ) ;
@ -297,13 +113,13 @@ public static partial class PortHelper
// Edit the icons in the apk
// Edit the icons in the apk
string resPath = apkDir + "/res" ;
string resPath = apkDir + "/res" ;
string origPath = utilDir + "/icon.png" ;
string origPath = utilDir + "/icon.png" ;
SaveAndroidIcon( origPath , 96 , resPath + "/drawable/icon.png" ) ;
HelperMethods. SaveAndroidIcon( origPath , 96 , resPath + "/drawable/icon.png" ) ;
SaveAndroidIcon( origPath , 72 , resPath + "/drawable-hdpi-v4/icon.png" ) ;
HelperMethods. SaveAndroidIcon( origPath , 72 , resPath + "/drawable-hdpi-v4/icon.png" ) ;
SaveAndroidIcon( origPath , 36 , resPath + "/drawable-ldpi-v4/icon.png" ) ;
HelperMethods. SaveAndroidIcon( origPath , 36 , resPath + "/drawable-ldpi-v4/icon.png" ) ;
SaveAndroidIcon( origPath , 48 , resPath + "/drawable-mdpi-v4/icon.png" ) ;
HelperMethods. SaveAndroidIcon( origPath , 48 , resPath + "/drawable-mdpi-v4/icon.png" ) ;
SaveAndroidIcon( origPath , 96 , resPath + "/drawable-xhdpi-v4/icon.png" ) ;
HelperMethods. SaveAndroidIcon( origPath , 96 , resPath + "/drawable-xhdpi-v4/icon.png" ) ;
SaveAndroidIcon( origPath , 144 , resPath + "/drawable-xxhdpi-v4/icon.png" ) ;
HelperMethods. SaveAndroidIcon( origPath , 144 , resPath + "/drawable-xxhdpi-v4/icon.png" ) ;
SaveAndroidIcon( origPath , 192 , resPath + "/drawable-xxxhdpi-v4/icon.png" ) ;
HelperMethods. SaveAndroidIcon( origPath , 192 , resPath + "/drawable-xxxhdpi-v4/icon.png" ) ;
// Hermite probably the best
// Hermite probably the best
@ -387,7 +203,7 @@ public static partial class PortHelper
}
}
//TODO: try to figure out if its possible to extract the name from the data.win file? They do have a displayname option last time I checked...
//TODO: try to figure out if its possible to extract the name from the data.win file? They do have a displayname option last time I checked...
public static void Port Windows ToMac( string inputRawZipPath , string outputRawZipPath , string modName , OutputHandlerDelegate outputDelegate = null )
public static void Port ToMac( string inputRawZipPath , string outputRawZipPath , string modName , OutputHandlerDelegate outputDelegate = null )
{
{
outputHandler = outputDelegate ;
outputHandler = outputDelegate ;
string baseTempDirectory = tmp + "/" + Path . GetFileNameWithoutExtension ( inputRawZipPath ) ;
string baseTempDirectory = tmp + "/" + Path . GetFileNameWithoutExtension ( inputRawZipPath ) ;
@ -404,7 +220,7 @@ public static partial class PortHelper
Directory . Delete ( baseTempDirectory , true ) ;
Directory . Delete ( baseTempDirectory , true ) ;
SendOutput ( "Copying Runner..." ) ;
SendOutput ( "Copying Runner..." ) ;
Directory . CreateDirectory ( contentsDir ) ;
Directory . CreateDirectory ( contentsDir ) ;
DirectoryCopy( utilDir + "/Contents" , contentsDir , true ) ;
HelperMethods. DirectoryCopy( utilDir + "/Contents" , contentsDir , true ) ;
// Extract mod to temp location
// Extract mod to temp location
SendOutput ( "Extracting Mac..." ) ;
SendOutput ( "Extracting Mac..." ) ;
@ -424,7 +240,7 @@ public static partial class PortHelper
Directory . Delete ( extractDirectory + "/lang/fonts" , true ) ;
Directory . Delete ( extractDirectory + "/lang/fonts" , true ) ;
// Lowercase every file first
// Lowercase every file first
LowercaseFolder( extractDirectory ) ;
HelperMethods. LowercaseFolder( extractDirectory ) ;
// Convert data.win to BC16 and get rid of not needed functions anymore
// Convert data.win to BC16 and get rid of not needed functions anymore
SendOutput ( "Editing data.win to change data.win BC version and functions..." ) ;
SendOutput ( "Editing data.win to change data.win BC version and functions..." ) ;
@ -458,7 +274,7 @@ public static partial class PortHelper
// Copy assets to the place where they belong to
// Copy assets to the place where they belong to
SendOutput ( "Copy files over..." ) ;
SendOutput ( "Copy files over..." ) ;
DirectoryCopy( extractDirectory , assetsDir , true ) ;
HelperMethods. DirectoryCopy( extractDirectory , assetsDir , true ) ;
// Edit config and plist to change display name
// Edit config and plist to change display name
SendOutput ( "Editing Runner references to AM2R..." ) ;
SendOutput ( "Editing Runner references to AM2R..." ) ;