@ -8,7 +8,6 @@ using System.IO.Compression;
using System.Linq ;
using System.Linq ;
using System.Text.RegularExpressions ;
using System.Text.RegularExpressions ;
using AM2RLauncherLib.XML ;
using AM2RLauncherLib.XML ;
using log4net.Util ;
namespace AM2RLauncherLib ;
namespace AM2RLauncherLib ;
@ -90,53 +89,59 @@ public static class Profile
/// <summary>
/// <summary>
/// Checks if a Zip file is a valid AM2R_1.1 zip.
/// Checks if a Zip file is a valid AM2R_1.1 zip.
/// </summary>
/// </summary>
/// <param name="zipPath">Full Path to the Zip file to check .</param>
/// <param name="zipPath">Full Path to the Zip file to validate .</param>
/// <returns><see cref="IsZipAM2R11ReturnCodes"/> detailing the result</returns>
/// <returns><see cref="IsZipAM2R11ReturnCodes"/> detailing the result</returns>
public static IsZipAM2R11ReturnCodes CheckIfZipIsAM2R11 ( string zipPath )
public static IsZipAM2R11ReturnCodes CheckIfZipIsAM2R11 ( string zipPath )
{
{
const string d3dHash = "86e39e9161c3d930d93822f1563c280d" ;
const string d3dHash = "86e39e9161c3d930d93822f1563c280d" ;
const string dataWinHash = "f2b84fe5ba64cb64e284be1066ca08ee" ;
const string dataWinHash = "f2b84fe5ba64cb64e284be1066ca08ee" ;
const string am2rHash = "15253f7a66d6ea3feef004ebbee9b438" ;
const string am2rHash = "15253f7a66d6ea3feef004ebbee9b438" ;
string tmpPath = Path . GetTempPath ( ) + Path . GetFileNameWithoutExtension ( zipPath ) ;
string tmpPath = Path . GetTempPath ( ) + Path . GetFileNameWithoutExtension ( zipPath ) ;
// Clean up in case folder exists already
// Clean up in case folder exists already
if ( Directory . Exists ( tmpPath ) )
if ( Directory . Exists ( tmpPath ) )
Directory . Delete ( tmpPath , true ) ;
Directory . Delete ( tmpPath , true ) ;
Directory . CreateDirectory ( tmpPath ) ;
Directory . CreateDirectory ( tmpPath ) ;
// Open archive
// Open archive
ZipArchive am2rZip = ZipFile . OpenRead ( zipPath ) ;
ZipArchive am2rZip = ZipFile . OpenRead ( zipPath ) ;
// Check if exe exists anywhere
// Check if exe exists anywhere
ZipArchiveEntry am2rExe = am2rZip . Entries . FirstOrDefault ( x = > x . FullName . Contains ( "AM2R.exe" ) ) ;
ZipArchiveEntry am2rExe = am2rZip . Entries . FirstOrDefault ( x = > x . FullName . Contains ( "AM2R.exe" ) ) ;
if ( am2rExe = = null )
if ( am2rExe = = null )
return IsZipAM2R11ReturnCodes . MissingOrInvalidAM2RExe ;
return IsZipAM2R11ReturnCodes . MissingOrInvalidAM2RExe ;
// Check if it's not in a subfolder. if it'd be in a subfolder, fullname would be "folder/AM2R.exe"
// Check if it's not in a subfolder. if it'd be in a subfolder, fullname would be "folder/AM2R.exe"
if ( am2rExe . FullName ! = "AM2R.exe" )
if ( am2rExe . FullName ! = "AM2R.exe" )
return IsZipAM2R11ReturnCodes . GameIsInASubfolder ;
return IsZipAM2R11ReturnCodes . GameIsInASubfolder ;
// Check validity
// Check validity
am2rExe . ExtractToFile ( $"{tmpPath}/{am2rExe.FullName}" ) ;
am2rExe . ExtractToFile ( $"{tmpPath}/{am2rExe.FullName}" ) ;
if ( HelperMethods . CalculateMD5 ( $"{tmpPath}/{am2rExe.FullName}" ) ! = am2rHash )
if ( HelperMethods . CalculateMD5 ( $"{tmpPath}/{am2rExe.FullName}" ) ! = am2rHash )
return IsZipAM2R11ReturnCodes . MissingOrInvalidAM2RExe ;
return IsZipAM2R11ReturnCodes . MissingOrInvalidAM2RExe ;
// Check if data.win exists / is valid
// Check if data.win exists / is valid
ZipArchiveEntry dataWin = am2rZip . Entries . FirstOrDefault ( x = > x . FullName = = "data.win" ) ;
ZipArchiveEntry dataWin = am2rZip . Entries . FirstOrDefault ( x = > x . FullName = = "data.win" ) ;
if ( dataWin = = null )
if ( dataWin = = null )
return IsZipAM2R11ReturnCodes . MissingOrInvalidDataWin ;
return IsZipAM2R11ReturnCodes . MissingOrInvalidDataWin ;
dataWin . ExtractToFile ( $"{tmpPath}/{dataWin.FullName}" ) ;
dataWin . ExtractToFile ( $"{tmpPath}/{dataWin.FullName}" ) ;
if ( HelperMethods . CalculateMD5 ( $"{tmpPath}/{dataWin.FullName}" ) ! = dataWinHash )
if ( HelperMethods . CalculateMD5 ( $"{tmpPath}/{dataWin.FullName}" ) ! = dataWinHash )
return IsZipAM2R11ReturnCodes . MissingOrInvalidDataWin ;
return IsZipAM2R11ReturnCodes . MissingOrInvalidDataWin ;
// Check if d3d.dll exists / is valid
// Check if d3d.dll exists / is valid
ZipArchiveEntry d3dx = am2rZip . Entries . FirstOrDefault ( x = > x . FullName = = "D3DX9_43.dll" ) ;
ZipArchiveEntry d3dx = am2rZip . Entries . FirstOrDefault ( x = > x . FullName = = "D3DX9_43.dll" ) ;
if ( d3dx = = null )
if ( d3dx = = null )
return IsZipAM2R11ReturnCodes . MissingOrInvalidD3DX943Dll ;
return IsZipAM2R11ReturnCodes . MissingOrInvalidD3DX943Dll ;
d3dx . ExtractToFile ( $"{tmpPath}/{d3dx.FullName}" ) ;
d3dx . ExtractToFile ( $"{tmpPath}/{d3dx.FullName}" ) ;
if ( HelperMethods . CalculateMD5 ( $"{tmpPath}/{d3dx.FullName}" ) ! = d3dHash )
if ( HelperMethods . CalculateMD5 ( $"{tmpPath}/{d3dx.FullName}" ) ! = d3dHash )
return IsZipAM2R11ReturnCodes . MissingOrInvalidD3DX943Dll ;
return IsZipAM2R11ReturnCodes . MissingOrInvalidD3DX943Dll ;
// Clean up
// Clean up
Directory . Delete ( tmpPath , true ) ;
Directory . Delete ( tmpPath , true ) ;
@ -146,7 +151,7 @@ public static class Profile
}
}
/// <summary>
/// <summary>
/// Git Pulls from the repository.
/// Git Pulls patching content from the repository.
/// </summary>
/// </summary>
public static void PullPatchData ( Func < TransferProgress , bool > transferProgressHandlerMethod )
public static void PullPatchData ( Func < TransferProgress , bool > transferProgressHandlerMethod )
{
{
@ -214,19 +219,15 @@ public static class Profile
if ( ! File . Exists ( $"{dir.FullName}/profile.xml" ) )
if ( ! File . Exists ( $"{dir.FullName}/profile.xml" ) )
continue ;
continue ;
ProfileXML prof = Serializer . Deserialize < ProfileXML > ( File . ReadAllText ( $"{dir.FullName}/profile.xml" ) ) ;
ProfileXML profile = Serializer . Deserialize < ProfileXML > ( File . ReadAllText ( $"{dir.FullName}/profile.xml" ) ) ;
profile . DataPath = $"/Mods/{dir.Name}" ;
// Safety check for non-installable profiles
// Safety check for non-installable profiles
if ( prof . Installable | | IsProfileInstalled ( prof ) )
{
prof . DataPath = $"/Mods/{dir.Name}" ;
profileList . Add ( prof ) ;
}
// If not installable and isn't installed, remove it
// If not installable and isn't installed, remove it
else if ( ! IsProfileInstalled ( prof ) )
if ( ! profile . Installable & & IsProfileInstalled ( profile ) )
{
DeleteProfile ( profile ) ;
prof . DataPath = $"/Mods/{dir.Name}" ;
else
DeleteProfile ( prof ) ;
profileList . Add ( profile ) ;
}
}
}
log . Info ( $"Loaded {profileList.Count} profile(s)." ) ;
log . Info ( $"Loaded {profileList.Count} profile(s)." ) ;
@ -242,6 +243,7 @@ public static class Profile
// Temporarily serialize and deserialize to essentially "clone" the variable as otherwise we'd modify references
// Temporarily serialize and deserialize to essentially "clone" the variable as otherwise we'd modify references
File . WriteAllText ( $"{Path.GetTempPath()}/{profile.Name}" , Serializer . Serialize < ProfileXML > ( profile ) ) ;
File . WriteAllText ( $"{Path.GetTempPath()}/{profile.Name}" , Serializer . Serialize < ProfileXML > ( profile ) ) ;
profile = Serializer . Deserialize < ProfileXML > ( File . ReadAllText ( $"{Path.GetTempPath()}/{profile.Name}" ) ) ;
profile = Serializer . Deserialize < ProfileXML > ( File . ReadAllText ( $"{Path.GetTempPath()}/{profile.Name}" ) ) ;
File . Delete ( $"{Path.GetTempPath()}/{profile.Name}" ) ;
string originalName = profile . Name ;
string originalName = profile . Name ;
// Change name to include version and be unique
// Change name to include version and be unique
@ -322,12 +324,11 @@ public static class Profile
{
{
log . Info ( $"Installing profile {profile.Name}..." ) ;
log . Info ( $"Installing profile {profile.Name}..." ) ;
string profilesHomePath = Core . ProfilesPath ;
string profilePath = $"{Core.ProfilesPath}/{profile.Name}" ;
string profilePath = $"{profilesHomePath}/{profile.Name}" ;
// Failsafe for Profiles directory
// Failsafe for Profiles directory
if ( ! Directory . Exists ( profilesHome Path) )
if ( ! Directory . Exists ( Core. Profiles Path) )
Directory . CreateDirectory ( profilesHome Path) ;
Directory . CreateDirectory ( Core. Profiles Path) ;
// This failsafe should NEVER get triggered, but Miepee's broken this too much for me to trust it otherwise.
// This failsafe should NEVER get triggered, but Miepee's broken this too much for me to trust it otherwise.
if ( Directory . Exists ( profilePath ) )
if ( Directory . Exists ( profilePath ) )
@ -336,7 +337,7 @@ public static class Profile
// Create profile directory
// Create profile directory
Directory . CreateDirectory ( profilePath ) ;
Directory . CreateDirectory ( profilePath ) ;
// Switch profilePath on Gtk
// Switch profilePath on Linux and Mac, as they need special handling
if ( OS . IsLinux )
if ( OS . IsLinux )
{
{
profilePath + = "/assets" ;
profilePath + = "/assets" ;
@ -346,22 +347,18 @@ public static class Profile
{
{
// Folder structure for mac is like this:
// Folder structure for mac is like this:
// am2r.app -> Contents
// am2r.app -> Contents
// -Frameworks (some libs)
// | -Frameworks (some libs)
// -MacOS (runner)
// | -MacOS (runner)
// -Resources (asset path)
// | -Resources (asset path)
profilePath + = "/AM2R.app/Contents" ;
profilePath + = "/AM2R.app/Contents" ;
Directory . CreateDirectory ( profilePath ) ;
Directory . CreateDirectory ( profilePath ) ;
Directory . CreateDirectory ( $"{profilePath}/MacOS" ) ;
Directory . CreateDirectory ( $"{profilePath}/MacOS" ) ;
Directory . CreateDirectory ( $"{profilePath}/Resources" ) ;
Directory . CreateDirectory ( $"{profilePath}/Resources" ) ;
profilePath + = "/Resources" ;
profilePath + = "/Resources" ;
log . Info ( "ProfileInstallation: Created folder structure." ) ;
}
}
// Extract 1.1
// Extract 1.1
ZipFile . ExtractToDirectory ( Core . AM2R11File , profilePath ) ;
ZipFile . ExtractToDirectory ( Core . AM2R11File , profilePath ) ;
// Extracted 1.1
progress . Report ( 33 ) ;
progress . Report ( 33 ) ;
log . Info ( "Profile folder created and AM2R_11.zip extracted." ) ;
log . Info ( "Profile folder created and AM2R_11.zip extracted." ) ;
@ -394,16 +391,16 @@ public static class Profile
log . Error ( $"{OS.Name} does not have valid runner / data.win names!" ) ;
log . Error ( $"{OS.Name} does not have valid runner / data.win names!" ) ;
}
}
// Patch runner and data file.
log . Info ( $"Attempting to patch in {profilePath}" ) ;
log . Info ( $"Attempting to patch in {profilePath}" ) ;
if ( OS . IsWindows )
if ( OS . IsWindows )
{
{
// Patch game executable
if ( profile . UsesYYC )
if ( profile . UsesYYC )
{
{
CrossPlatformOperations . ApplyXdeltaPatch ( $"{profilePath}/data.win" , $"{dataPath}/AM2R.xdelta" , $"{profilePath}/{exe}" ) ;
CrossPlatformOperations . ApplyXdeltaPatch ( $"{profilePath}/data.win" , $"{dataPath}/AM2R.xdelta" , $"{profilePath}/{exe}" ) ;
// Delete 1.1's data.win, we don't need it anymore!
// Delete 1.1's data.win, we don't need it anymore!
// TODO: *theoretically* if someone would make some game like serradius in gms2 and push that as a yyc am2r mod, this *will* break!
File . Delete ( $"{profilePath}/data.win" ) ;
File . Delete ( $"{profilePath}/data.win" ) ;
}
}
else
else
@ -412,11 +409,12 @@ public static class Profile
CrossPlatformOperations . ApplyXdeltaPatch ( $"{profilePath}/AM2R.exe" , $"{dataPath}/AM2R.xdelta" , $"{profilePath}/{exe}" ) ;
CrossPlatformOperations . ApplyXdeltaPatch ( $"{profilePath}/AM2R.exe" , $"{dataPath}/AM2R.xdelta" , $"{profilePath}/{exe}" ) ;
}
}
}
}
else if ( OS . IsUnix ) // YYC and VM look exactly the same on Linux and Mac so we're all good here.
// YYC and VM look exactly the same on Linux and Mac so we're all good here.
else if ( OS . IsUnix )
{
{
CrossPlatformOperations . ApplyXdeltaPatch ( $"{profilePath}/data.win" , $"{dataPath}/game.xdelta" , $"{profilePath}/{dataWin}" ) ;
CrossPlatformOperations . ApplyXdeltaPatch ( $"{profilePath}/data.win" , $"{dataPath}/game.xdelta" , $"{profilePath}/{dataWin}" ) ;
CrossPlatformOperations . ApplyXdeltaPatch ( $"{profilePath}/AM2R.exe" , $"{dataPath}/AM2R.xdelta" , $"{profilePath}/{exe}" ) ;
CrossPlatformOperations . ApplyXdeltaPatch ( $"{profilePath}/AM2R.exe" , $"{dataPath}/AM2R.xdelta" , $"{profilePath}/{exe}" ) ;
// Just in case the resulting file isn't chmod-ed ...
// Just in case the resulting file isn't set as executable ...
Process . Start ( "chmod" , $"+x \" { profilePath } / { exe } \ "" ) ? . WaitForExit ( ) ;
Process . Start ( "chmod" , $"+x \" { profilePath } / { exe } \ "" ) ? . WaitForExit ( ) ;
// These are not needed by linux or Mac at all, so we delete them
// These are not needed by linux or Mac at all, so we delete them
@ -426,7 +424,7 @@ public static class Profile
// Move exe one directory out on Linux, move to MacOS folder instead on Mac
// Move exe one directory out on Linux, move to MacOS folder instead on Mac
if ( OS . IsLinux )
if ( OS . IsLinux )
File . Move ( $"{profilePath}/{exe}" , $"{profilePath .Substring(0, profilePath .LastIndexOf("/ "))} /{exe}") ;
File . Move ( $"{profilePath}/{exe}" , $"{profilePath }/ ../{exe}") ;
else
else
File . Move ( $"{profilePath}/{exe}" , $"{profilePath.Replace(" Resources ", " MacOS ")}/{exe}" ) ;
File . Move ( $"{profilePath}/{exe}" , $"{profilePath.Replace(" Resources ", " MacOS ")}/{exe}" ) ;
}
}
@ -447,7 +445,6 @@ public static class Profile
if ( ! profile . UsesCustomMusic & & useHqMusic )
if ( ! profile . UsesCustomMusic & & useHqMusic )
HelperMethods . DirectoryCopy ( $"{Core.PatchDataPath}/data/HDR_HQ_in-game_music" , profilePath ) ;
HelperMethods . DirectoryCopy ( $"{Core.PatchDataPath}/data/HDR_HQ_in-game_music" , profilePath ) ;
// Linux post-process
// Linux post-process
if ( OS . IsLinux )
if ( OS . IsLinux )
{
{
@ -473,9 +470,10 @@ public static class Profile
File . Copy ( $"{profilePath}/{exe}" , $"{profilePath}/AM2R.AppDir/usr/bin/{exe}" ) ;
File . Copy ( $"{profilePath}/{exe}" , $"{profilePath}/AM2R.AppDir/usr/bin/{exe}" ) ;
progress . Report ( 66 ) ;
progress . Report ( 66 ) ;
log . Info ( " Gtk- specific formatting finished.") ;
log . Info ( " Linux specific formatting finished.") ;
// Temp save the currentWorkingDirectory and console.error, change it to profilePath and null, call the script, and change it back.
// Temp save the currentWorkingDirectory and STDERR, change it to profilePath and null, call the tool, and change it back.
// Reason why STDERR is changed is because the tool prints some output to there that we don't want
string workingDir = Directory . GetCurrentDirectory ( ) ;
string workingDir = Directory . GetCurrentDirectory ( ) ;
TextWriter cliError = Console . Error ;
TextWriter cliError = Console . Error ;
Directory . SetCurrentDirectory ( profilePath ) ;
Directory . SetCurrentDirectory ( profilePath ) ;
@ -512,17 +510,17 @@ public static class Profile
File . Copy ( $"{Core.PatchDataPath}/data/PkgInfo" , $"{profilePath.Replace(" Resources ", " ")}/PkgInfo" , true ) ;
File . Copy ( $"{Core.PatchDataPath}/data/PkgInfo" , $"{profilePath.Replace(" Resources ", " ")}/PkgInfo" , true ) ;
//Put profilePath back to what it was before
//Put profilePath back to what it was before
profilePath = $"{ profilesHome Path}/{profile.Name}";
profilePath = $"{ Core.Profiles Path}/{profile.Name}";
}
}
// Copy profile.xml so we can grab data to compare for updates later!
// Copy profile.xml so we can grab data to compare for updates later!
// tldr; check if we're in PatchData or not
// check if we're in PatchData or not, as we need to search for profile.xml in different locations.
if ( new DirectoryInfo ( dataPath ) . Parent ? . Name = = "PatchData" )
if ( new DirectoryInfo ( dataPath ) . Parent ? . Name = = "PatchData" )
File . Copy ( $"{dataPath}/../profile.xml" , $"{profilePath}/profile.xml" ) ;
File . Copy ( $"{dataPath}/../profile.xml" , $"{profilePath}/profile.xml" ) ;
else
else
File . Copy ( $"{dataPath}/profile.xml" , $"{profilePath}/profile.xml" ) ;
File . Copy ( $"{dataPath}/profile.xml" , $"{profilePath}/profile.xml" ) ;
// Installed datafiles
// Done
progress . Report ( 100 ) ;
progress . Report ( 100 ) ;
log . Info ( $"Successfully installed profile {profile.Name}." ) ;
log . Info ( $"Successfully installed profile {profile.Name}." ) ;
}
}
@ -560,10 +558,12 @@ public static class Profile
log . Info ( $"Creating Android APK for profile {profile.Name}." ) ;
log . Info ( $"Creating Android APK for profile {profile.Name}." ) ;
// Create working dir after some cleanup
// Create working dir after some cleanup
string apktoolPath = $"{Core.PatchDataPath}/utilities/android/apktool.jar" ,
string apktoolPath = $"{Core.PatchDataPath}/utilities/android/apktool.jar" ;
uberPath = $"{Core.PatchDataPath}/utilities/android/uber-apk-signer.jar" ,
string uberPath = $"{Core.PatchDataPath}/utilities/android/uber-apk-signer.jar" ;
tempDir = new DirectoryInfo ( $"{CrossPlatformOperations.CurrentPath}/temp" ) . FullName ,
string tempDir = new DirectoryInfo ( $"{CrossPlatformOperations.CurrentPath}/temp" ) . FullName ;
dataPath = CrossPlatformOperations . CurrentPath + profile . DataPath ;
string dataPath = CrossPlatformOperations . CurrentPath + profile . DataPath ;
// Clean up in case Directory exists already
if ( Directory . Exists ( tempDir ) )
if ( Directory . Exists ( tempDir ) )
Directory . Delete ( tempDir , true ) ;
Directory . Delete ( tempDir , true ) ;
Directory . CreateDirectory ( tempDir ) ;
Directory . CreateDirectory ( tempDir ) ;
@ -583,6 +583,7 @@ public static class Profile
if ( useHqMusic )
if ( useHqMusic )
HelperMethods . DirectoryCopy ( $"{Core.PatchDataPath}/data/HDR_HQ_in-game_music" , workingDir ) ;
HelperMethods . DirectoryCopy ( $"{Core.PatchDataPath}/data/HDR_HQ_in-game_music" , workingDir ) ;
// Yes, I'm aware this is dumb. If you've got any better ideas for how to copy a seemingly randomly named .ini from this folder to the APK, please let me know.
// Yes, I'm aware this is dumb. If you've got any better ideas for how to copy a seemingly randomly named .ini from this folder to the APK, please let me know.
foreach ( FileInfo file in new DirectoryInfo ( dataPath ) . GetFiles ( ) . Where ( f = > f . Name . EndsWith ( "ini" ) ) )
foreach ( FileInfo file in new DirectoryInfo ( dataPath ) . GetFiles ( ) . Where ( f = > f . Name . EndsWith ( "ini" ) ) )
File . Copy ( file . FullName , $"{workingDir}/{file.Name}" ) ;
File . Copy ( file . FullName , $"{workingDir}/{file.Name}" ) ;
@ -637,6 +638,7 @@ public static class Profile
/// </summary>
/// </summary>
public static void RunGame ( ProfileXML profile , bool useLogging , string envVars = "" )
public static void RunGame ( ProfileXML profile , bool useLogging , string envVars = "" )
{
{
//TODO: double check this and clean
// These are used on both windows and linux for game logging
// These are used on both windows and linux for game logging
string savePath = OS . IsWindows ? profile . SaveLocation . Replace ( "%localappdata%" , Environment . GetEnvironmentVariable ( "LOCALAPPDATA" ) )
string savePath = OS . IsWindows ? profile . SaveLocation . Replace ( "%localappdata%" , Environment . GetEnvironmentVariable ( "LOCALAPPDATA" ) )
: profile . SaveLocation . Replace ( "~" , CrossPlatformOperations . Home ) ;
: profile . SaveLocation . Replace ( "~" , CrossPlatformOperations . Home ) ;