2019-11-03 16:43:43 +00:00
using System ;
using System.Collections.Generic ;
using System.IO ;
using System.IO.Compression ;
using System.Linq ;
2019-11-04 16:04:37 +00:00
using System.Security.Cryptography ;
2019-11-03 16:43:43 +00:00
using System.Text ;
2019-11-09 20:35:08 +00:00
using Microsoft.WindowsAPICodePack.Shell ;
2019-11-03 16:43:43 +00:00
using Wabbajack.Common ;
using Wabbajack.Lib.CompilationSteps ;
2019-11-04 16:04:37 +00:00
using Wabbajack.Lib.Downloaders ;
2019-11-09 15:10:31 +00:00
using Wabbajack.Lib.ModListRegistry ;
2019-11-04 16:04:37 +00:00
using Wabbajack.Lib.NexusApi ;
2019-11-15 13:06:34 +00:00
using Wabbajack.VirtualFileSystem ;
2019-11-09 15:10:31 +00:00
using File = Alphaleonis . Win32 . Filesystem . File ;
2019-11-03 16:43:43 +00:00
namespace Wabbajack.Lib
{
public class VortexCompiler : ACompiler
{
2019-11-09 14:10:28 +00:00
public Game Game { get ; }
2019-11-03 16:43:43 +00:00
public string GameName { get ; }
2019-11-12 17:54:48 +00:00
public string VortexFolder { get ; set ; }
public string StagingFolder { get ; set ; }
public string DownloadsFolder { get ; set ; }
2019-11-03 16:43:43 +00:00
public bool IgnoreMissingFiles { get ; set ; }
2019-11-17 03:09:46 +00:00
public const string StagingMarkerName = "__vortex_staging_folder" ;
public const string DownloadMarkerName = "__vortex_downloads_folder" ;
2019-11-16 22:10:13 +00:00
public VortexCompiler ( Game game , string gamePath , string vortexFolder , string downloadsFolder , string stagingFolder )
2019-11-03 16:43:43 +00:00
{
ModManager = ModManager . Vortex ;
// TODO: only for testing
IgnoreMissingFiles = true ;
GamePath = gamePath ;
2019-11-16 22:10:13 +00:00
GameName = GameRegistry . Games [ game ] . NexusName ;
this . VortexFolder = vortexFolder ;
this . DownloadsFolder = downloadsFolder ;
this . StagingFolder = stagingFolder ;
2019-11-17 04:30:22 +00:00
Queue = new WorkQueue ( ) ;
VFS = new Context ( Queue ) ;
2019-11-03 16:43:43 +00:00
ModListOutputFolder = "output_folder" ;
// TODO: add custom modlist name
ModListOutputFile = $"VORTEX_TEST_MODLIST{ExtensionManager.Extension}" ;
}
public override void Info ( string msg )
{
Utils . Log ( msg ) ;
}
public override void Status ( string msg )
{
2019-11-17 04:16:42 +00:00
Queue . Report ( msg , 0 ) ;
2019-11-03 16:43:43 +00:00
}
public override void Error ( string msg )
{
Utils . Log ( msg ) ;
throw new Exception ( msg ) ;
}
internal override string IncludeFile ( byte [ ] data )
{
var id = Guid . NewGuid ( ) . ToString ( ) ;
File . WriteAllBytes ( Path . Combine ( ModListOutputFolder , id ) , data ) ;
return id ;
}
internal override string IncludeFile ( string data )
{
var id = Guid . NewGuid ( ) . ToString ( ) ;
File . WriteAllText ( Path . Combine ( ModListOutputFolder , id ) , data ) ;
return id ;
}
public override bool Compile ( )
{
Info ( $"Starting Vortex compilation for {GameName} at {GamePath} with staging folder at {StagingFolder} and downloads folder at {DownloadsFolder}." ) ;
2019-11-04 16:04:37 +00:00
Info ( "Starting pre-compilation steps" ) ;
CreateMetaFiles ( ) ;
Info ( $"Indexing {StagingFolder}" ) ;
2019-11-16 00:01:37 +00:00
VFS . AddRoot ( StagingFolder ) ;
2019-11-04 16:04:37 +00:00
2019-11-03 16:43:43 +00:00
Info ( $"Indexing {GamePath}" ) ;
2019-11-16 00:01:37 +00:00
VFS . AddRoot ( GamePath ) ;
2019-11-03 16:43:43 +00:00
Info ( $"Indexing {DownloadsFolder}" ) ;
2019-11-16 00:01:37 +00:00
VFS . AddRoot ( DownloadsFolder ) ;
2019-11-03 16:43:43 +00:00
2019-11-09 20:35:08 +00:00
AddExternalFolder ( ) ;
2019-11-03 16:43:43 +00:00
Info ( "Cleaning output folder" ) ;
if ( Directory . Exists ( ModListOutputFolder ) ) Directory . Delete ( ModListOutputFolder , true ) ;
Directory . CreateDirectory ( ModListOutputFolder ) ;
2019-11-04 16:04:37 +00:00
IEnumerable < RawSourceFile > vortexStagingFiles = Directory . EnumerateFiles ( StagingFolder , "*" , SearchOption . AllDirectories )
2019-11-17 03:09:46 +00:00
. Where ( p = > p . FileExists ( ) & & p ! = StagingMarkerName )
2019-11-15 13:06:34 +00:00
. Select ( p = > new RawSourceFile ( VFS . Index . ByRootPath [ p ] )
2019-11-04 16:04:37 +00:00
{ Path = p . RelativeTo ( StagingFolder ) } ) ;
IEnumerable < RawSourceFile > vortexDownloads = Directory . EnumerateFiles ( DownloadsFolder , "*" , SearchOption . AllDirectories )
. Where ( p = > p . FileExists ( ) )
2019-11-15 13:06:34 +00:00
. Select ( p = > new RawSourceFile ( VFS . Index . ByRootPath [ p ] )
2019-11-04 16:04:37 +00:00
{ Path = p . RelativeTo ( DownloadsFolder ) } ) ;
2019-11-03 16:43:43 +00:00
2019-11-04 16:04:37 +00:00
IEnumerable < RawSourceFile > gameFiles = Directory . EnumerateFiles ( GamePath , "*" , SearchOption . AllDirectories )
2019-11-03 16:43:43 +00:00
. Where ( p = > p . FileExists ( ) )
2019-11-15 13:06:34 +00:00
. Select ( p = > new RawSourceFile ( VFS . Index . ByRootPath [ p ] )
2019-11-04 12:30:02 +00:00
{ Path = Path . Combine ( Consts . GameFolderFilesDir , p . RelativeTo ( GamePath ) ) } ) ;
2019-11-03 16:43:43 +00:00
Info ( "Indexing Archives" ) ;
IndexedArchives = Directory . EnumerateFiles ( DownloadsFolder )
2019-11-15 13:06:34 +00:00
. Where ( f = > File . Exists ( f + ".meta" ) )
2019-11-03 16:43:43 +00:00
. Select ( f = > new IndexedArchive
{
2019-11-15 13:06:34 +00:00
File = VFS . Index . ByRootPath [ f ] ,
2019-11-04 12:30:02 +00:00
Name = Path . GetFileName ( f ) ,
2019-11-15 13:06:34 +00:00
IniData = ( f + ".meta" ) . LoadIniFile ( ) ,
Meta = File . ReadAllText ( f + ".meta" )
2019-11-03 16:43:43 +00:00
} )
. ToList ( ) ;
Info ( "Indexing Files" ) ;
2019-11-15 13:06:34 +00:00
IndexedFiles = IndexedArchives . SelectMany ( f = > f . File . ThisAndAllChildren )
. OrderBy ( f = > f . NestingFactor )
2019-11-03 16:43:43 +00:00
. GroupBy ( f = > f . Hash )
. ToDictionary ( f = > f . Key , f = > f . AsEnumerable ( ) ) ;
Info ( "Searching for mod files" ) ;
2019-11-04 16:04:37 +00:00
AllFiles = vortexStagingFiles . Concat ( vortexDownloads )
. Concat ( gameFiles )
. DistinctBy ( f = > f . Path )
. ToList ( ) ;
2019-11-03 16:43:43 +00:00
Info ( $"Found {AllFiles.Count} files to build into mod list" ) ;
Info ( "Verifying destinations" ) ;
List < IGrouping < string , RawSourceFile > > dups = AllFiles . GroupBy ( f = > f . Path )
. Where ( fs = > fs . Count ( ) > 1 )
. Select ( fs = >
{
2019-11-04 12:30:02 +00:00
Utils . Log ( $"Duplicate files installed to {fs.Key} from : {string.Join(" , ", fs.Select(f => f.AbsolutePath))}" ) ;
2019-11-03 16:43:43 +00:00
return fs ;
} ) . ToList ( ) ;
if ( dups . Count > 0 )
{
Error ( $"Found {dups.Count} duplicates, exiting" ) ;
}
IEnumerable < ICompilationStep > stack = MakeStack ( ) ;
Info ( "Running Compilation Stack" ) ;
2019-11-17 04:16:42 +00:00
List < Directive > results = AllFiles . PMap ( Queue , f = > RunStack ( stack . Where ( s = > s ! = null ) , f ) ) . ToList ( ) ;
2019-11-03 16:43:43 +00:00
IEnumerable < NoMatch > noMatch = results . OfType < NoMatch > ( ) . ToList ( ) ;
Info ( $"No match for {noMatch.Count()} files" ) ;
foreach ( var file in noMatch )
Info ( $" {file.To}" ) ;
if ( noMatch . Any ( ) )
{
if ( IgnoreMissingFiles )
{
Info ( "Continuing even though files were missing at the request of the user." ) ;
}
else
{
Info ( "Exiting due to no way to compile these files" ) ;
return false ;
}
}
InstallDirectives = results . Where ( i = > ! ( i is IgnoredDirectly ) ) . ToList ( ) ;
// TODO: nexus stuff
/ * Info ( "Getting Nexus api_key, please click authorize if a browser window appears" ) ;
if ( IndexedArchives . Any ( a = > a . IniData ? . General ? . gameName ! = null ) )
{
var nexusClient = new NexusApiClient ( ) ;
if ( ! nexusClient . IsPremium ) Error ( $"User {nexusClient.Username} is not a premium Nexus user, so we cannot access the necessary API calls, cannot continue" ) ;
}
* /
GatherArchives ( ) ;
ModList = new ModList
{
Archives = SelectedArchives ,
ModManager = ModManager . Vortex ,
2019-11-09 15:10:31 +00:00
Directives = InstallDirectives ,
GameType = Game
2019-11-03 16:43:43 +00:00
} ;
ExportModList ( ) ;
Info ( "Done Building ModList" ) ;
return true ;
}
2019-11-09 20:35:08 +00:00
/// <summary>
/// Some have mods outside their game folder located
/// </summary>
private void AddExternalFolder ( )
{
var currentGame = GameRegistry . Games [ Game ] ;
if ( currentGame . AdditionalFolders = = null | | currentGame . AdditionalFolders . Count = = 0 ) return ;
currentGame . AdditionalFolders . Do ( f = >
{
var path = f . Replace ( "%documents%" , KnownFolders . Documents . Path ) ;
if ( ! Directory . Exists ( path ) ) return ;
Info ( $"Indexing {path}" ) ;
VFS . AddRoot ( path ) ;
} ) ;
}
2019-11-03 16:43:43 +00:00
private void ExportModList ( )
{
Utils . Log ( $"Exporting ModList to: {ModListOutputFolder}" ) ;
// using JSON for better debugging
ModList . ToJSON ( Path . Combine ( ModListOutputFolder , "modlist.json" ) ) ;
//ModList.ToCERAS(Path.Combine(ModListOutputFolder, "modlist"), ref CerasConfig.Config);
if ( File . Exists ( ModListOutputFile ) )
File . Delete ( ModListOutputFile ) ;
using ( var fs = new FileStream ( ModListOutputFile , FileMode . Create ) )
{
using ( var za = new ZipArchive ( fs , ZipArchiveMode . Create ) )
{
Directory . EnumerateFiles ( ModListOutputFolder , "*.*" )
2019-11-04 12:30:02 +00:00
. DoProgress ( "Compressing ModList" ,
2019-11-03 16:43:43 +00:00
f = >
{
var ze = za . CreateEntry ( Path . GetFileName ( f ) ) ;
using ( var os = ze . Open ( ) )
using ( var ins = File . OpenRead ( f ) )
{
ins . CopyTo ( os ) ;
}
} ) ;
}
}
2019-11-09 15:10:31 +00:00
Utils . Log ( "Exporting ModList metadata" ) ;
var metadata = new ModlistMetadata . DownloadMetadata
{
Size = File . GetSize ( ModListOutputFile ) ,
Hash = ModListOutputFile . FileHash ( ) ,
NumberOfArchives = ModList . Archives . Count ,
SizeOfArchives = ModList . Archives . Sum ( a = > a . Size ) ,
NumberOfInstalledFiles = ModList . Directives . Count ,
SizeOfInstalledFiles = ModList . Directives . Sum ( a = > a . Size )
} ;
metadata . ToJSON ( ModListOutputFile + ".meta.json" ) ;
2019-11-03 16:43:43 +00:00
Utils . Log ( "Removing ModList staging folder" ) ;
2019-11-04 12:30:02 +00:00
//Directory.Delete(ModListOutputFolder, true);
2019-11-03 16:43:43 +00:00
}
2019-11-09 15:10:31 +00:00
/ * private void GenerateReport ( )
{
string css ;
using ( var cssStream = Utils . GetResourceStream ( "Wabbajack.Lib.css-min.css" ) )
using ( var reader = new StreamReader ( cssStream ) )
{
css = reader . ReadToEnd ( ) ;
}
using ( var fs = File . OpenWrite ( $"{ModList.Name}.md" ) )
{
fs . SetLength ( 0 ) ;
using ( var reporter = new ReportBuilder ( fs , ModListOutputFolder ) )
{
reporter . Build ( this , ModList ) ;
}
}
} * /
2019-11-04 16:04:37 +00:00
private void CreateMetaFiles ( )
{
2019-11-11 13:03:48 +00:00
Utils . Log ( "Getting Nexus api_key, please click authorize if a browser window appears" ) ;
var nexusClient = new NexusApiClient ( ) ;
2019-11-04 16:04:37 +00:00
Directory . EnumerateFiles ( DownloadsFolder , "*" , SearchOption . TopDirectoryOnly )
2019-11-11 13:03:48 +00:00
. Where ( f = > File . Exists ( f ) & & Path . GetExtension ( f ) ! = ".meta" & & ! File . Exists ( f + ".meta" ) )
2019-11-04 16:04:37 +00:00
. Do ( f = >
{
2019-11-09 15:19:08 +00:00
Utils . Log ( $"Trying to create meta file for {Path.GetFileName(f)}" ) ;
2019-11-04 16:04:37 +00:00
var metaString = $"[General]\n" +
$"repository=Nexus\n" +
$"installed=true\n" +
$"uninstalled=false\n" +
$"paused=false\n" +
$"removed=false\n" +
$"gameName={GameName}\n" ;
2019-11-09 15:19:08 +00:00
string hash ;
2019-11-04 16:04:37 +00:00
using ( var md5 = MD5 . Create ( ) )
using ( var stream = File . OpenRead ( f ) )
{
2019-11-09 15:19:08 +00:00
Utils . Log ( $"Calculating hash for {Path.GetFileName(f)}" ) ;
2019-11-04 16:04:37 +00:00
byte [ ] cH = md5 . ComputeHash ( stream ) ;
hash = BitConverter . ToString ( cH ) . Replace ( "-" , "" ) . ToLowerInvariant ( ) ;
2019-11-09 15:19:08 +00:00
Utils . Log ( $"Hash is {hash}" ) ;
2019-11-04 16:04:37 +00:00
}
2019-11-09 15:19:08 +00:00
List < MD5Response > md5Response = nexusClient . GetModInfoFromMD5 ( Game , hash ) ;
if ( md5Response . Count > = 1 )
{
var modInfo = md5Response [ 0 ] . mod ;
metaString + = $"modID={modInfo.mod_id}\ndescription={NexusApiUtils.FixupSummary(modInfo.summary)}\n" +
$"modName={modInfo.name}\nfileID={md5Response[0].file_details.file_id}" ;
File . WriteAllText ( f + ".meta" , metaString , Encoding . UTF8 ) ;
}
else
{
Error ( "Error while getting information from nexusmods via MD5 hash!" ) ;
}
2019-11-04 16:04:37 +00:00
} ) ;
}
2019-11-03 16:43:43 +00:00
private void GatherArchives ( )
{
Info ( "Building a list of archives based on the files required" ) ;
var shas = InstallDirectives . OfType < FromArchive > ( )
. Select ( a = > a . ArchiveHashPath [ 0 ] )
. Distinct ( ) ;
var archives = IndexedArchives . OrderByDescending ( f = > f . File . LastModified )
. GroupBy ( f = > f . File . Hash )
. ToDictionary ( f = > f . Key , f = > f . First ( ) ) ;
2019-11-17 04:16:42 +00:00
SelectedArchives = shas . PMap ( Queue , sha = > ResolveArchive ( sha , archives ) ) ;
2019-11-03 16:43:43 +00:00
}
private Archive ResolveArchive ( string sha , IDictionary < string , IndexedArchive > archives )
{
if ( archives . TryGetValue ( sha , out var found ) )
{
2019-11-04 16:04:37 +00:00
if ( found . IniData = = null )
Error ( $"No download metadata found for {found.Name}, please use MO2 to query info or add a .meta file and try again." ) ;
2019-11-03 16:43:43 +00:00
var result = new Archive ( ) ;
2019-11-04 16:04:37 +00:00
result . State = ( AbstractDownloadState ) DownloadDispatcher . ResolveArchive ( found . IniData ) ;
if ( result . State = = null )
Error ( $"{found.Name} could not be handled by any of the downloaders" ) ;
2019-11-03 16:43:43 +00:00
result . Name = found . Name ;
result . Hash = found . File . Hash ;
2019-11-04 16:04:37 +00:00
result . Meta = found . Meta ;
2019-11-03 16:43:43 +00:00
result . Size = found . File . Size ;
2019-11-04 16:04:37 +00:00
Info ( $"Checking link for {found.Name}" ) ;
if ( ! result . State . Verify ( ) )
Error (
$"Unable to resolve link for {found.Name}. If this is hosted on the Nexus the file may have been removed." ) ;
2019-11-03 16:43:43 +00:00
return result ;
}
Error ( $"No match found for Archive sha: {sha} this shouldn't happen" ) ;
return null ;
}
public override Directive RunStack ( IEnumerable < ICompilationStep > stack , RawSourceFile source )
{
Utils . Status ( $"Compiling {source.Path}" ) ;
foreach ( var step in stack )
{
var result = step . Run ( source ) ;
if ( result ! = null ) return result ;
}
throw new InvalidDataException ( "Data fell out of the compilation stack" ) ;
}
public override IEnumerable < ICompilationStep > GetStack ( )
{
2019-11-14 19:20:08 +00:00
var s = Consts . TestMode ? DownloadsFolder : VortexFolder ;
var userConfig = Path . Combine ( s , "compilation_stack.yml" ) ;
2019-11-03 16:43:43 +00:00
if ( File . Exists ( userConfig ) )
return Serialization . Deserialize ( File . ReadAllText ( userConfig ) , this ) ;
IEnumerable < ICompilationStep > stack = MakeStack ( ) ;
2019-11-14 19:20:08 +00:00
File . WriteAllText ( Path . Combine ( s , "_current_compilation_stack.yml" ) ,
2019-11-03 16:43:43 +00:00
Serialization . Serialize ( stack ) ) ;
return stack ;
}
public override IEnumerable < ICompilationStep > MakeStack ( )
{
Utils . Log ( "Generating compilation stack" ) ;
return new List < ICompilationStep >
{
//new IncludePropertyFiles(this),
2019-11-04 12:47:41 +00:00
new IncludeVortexDeployment ( this ) ,
2019-11-04 16:04:37 +00:00
new IncludeRegex ( this , "^*\\.meta" ) ,
2019-11-11 13:03:48 +00:00
new IgnoreVortex ( this ) ,
2019-11-09 18:57:29 +00:00
Game = = Game . DarkestDungeon ? new IncludeRegex ( this , "project\\.xml$" ) : null ,
2019-11-17 03:09:46 +00:00
new IgnoreStartsWith ( this , StagingFolder ) ,
new IgnoreEndsWith ( this , StagingFolder ) ,
2019-11-03 16:43:43 +00:00
2019-11-04 12:47:41 +00:00
new IgnoreGameFiles ( this ) ,
2019-11-03 16:43:43 +00:00
new DirectMatch ( this ) ,
new IgnoreGameFiles ( this ) ,
new IgnoreWabbajackInstallCruft ( this ) ,
new DropAll ( this )
} ;
}
2019-11-16 22:10:13 +00:00
public static string TypicalVortexFolder ( )
{
return Path . Combine ( Environment . GetFolderPath ( Environment . SpecialFolder . ApplicationData ) , "Vortex" ) ;
}
2019-11-17 03:09:46 +00:00
public static string RetrieveDownloadLocation ( Game game , string vortexFolderPath = null )
2019-11-16 22:10:13 +00:00
{
vortexFolderPath = vortexFolderPath ? ? TypicalVortexFolder ( ) ;
2019-11-17 03:09:46 +00:00
return Path . Combine ( vortexFolderPath , "downloads" , GameRegistry . Games [ game ] . NexusName ) ;
2019-11-16 22:10:13 +00:00
}
2019-11-17 03:09:46 +00:00
public static string RetrieveStagingLocation ( Game game , string vortexFolderPath = null )
2019-11-16 22:10:13 +00:00
{
2019-11-17 03:09:46 +00:00
vortexFolderPath = vortexFolderPath ? ? TypicalVortexFolder ( ) ;
var gameName = GameRegistry . Games [ game ] . NexusName ;
return Path . Combine ( vortexFolderPath , gameName , "mods" ) ;
2019-11-16 22:10:13 +00:00
}
2019-11-17 03:09:46 +00:00
public static IErrorResponse IsValidBaseDownloadsFolder ( string path )
2019-11-16 22:10:13 +00:00
{
2019-11-17 03:09:46 +00:00
if ( ! Directory . Exists ( path ) ) return ErrorResponse . Fail ( $"Path does not exist: {path}" ) ;
if ( Directory . EnumerateFiles ( path , DownloadMarkerName , SearchOption . TopDirectoryOnly ) . Any ( ) ) return ErrorResponse . Success ;
return ErrorResponse . Fail ( $"Folder must contain {DownloadMarkerName} file" ) ;
2019-11-16 22:10:13 +00:00
}
2019-11-17 03:09:46 +00:00
public static IErrorResponse IsValidDownloadsFolder ( string path )
2019-11-16 22:10:13 +00:00
{
2019-11-17 03:09:46 +00:00
return IsValidBaseDownloadsFolder ( Path . GetDirectoryName ( path ) ) ;
}
public static IErrorResponse IsValidBaseStagingFolder ( string path )
{
if ( ! Directory . Exists ( path ) ) return ErrorResponse . Fail ( $"Path does not exist: {path}" ) ;
if ( Directory . EnumerateFiles ( path , StagingMarkerName , SearchOption . TopDirectoryOnly ) . Any ( ) ) return ErrorResponse . Success ;
return ErrorResponse . Fail ( $"Folder must contain {StagingMarkerName} file" ) ;
}
public static IErrorResponse IsValidStagingFolder ( string path )
{
return IsValidBaseStagingFolder ( Path . GetDirectoryName ( path ) ) ;
2019-11-16 22:10:13 +00:00
}
2019-11-03 16:43:43 +00:00
}
}