2019-09-12 23:44:35 +00:00
using System ;
2019-07-26 20:59:14 +00:00
using System.Collections.Concurrent ;
2019-07-21 04:40:54 +00:00
using System.Collections.Generic ;
2019-08-30 23:57:56 +00:00
using System.Diagnostics ;
2019-07-21 04:40:54 +00:00
using System.IO ;
using System.Linq ;
2019-07-21 22:47:17 +00:00
using System.Reflection ;
2019-09-12 23:44:35 +00:00
using System.Runtime.Serialization.Formatters.Binary ;
2019-07-21 04:40:54 +00:00
using System.Text ;
using System.Text.RegularExpressions ;
2019-07-24 04:31:08 +00:00
using System.Web ;
2019-08-30 23:57:56 +00:00
using CommonMark ;
2019-09-12 23:44:35 +00:00
using Compression.BSA ;
using K4os.Compression.LZ4 ;
using K4os.Compression.LZ4.Streams ;
using Newtonsoft.Json ;
using VFS ;
2019-07-21 04:40:54 +00:00
using Wabbajack.Common ;
2019-08-05 23:58:42 +00:00
using static Wabbajack . NexusAPI ;
2019-09-18 21:33:23 +00:00
using Directory = Alphaleonis . Win32 . Filesystem . Directory ;
using File = Alphaleonis . Win32 . Filesystem . File ;
using FileInfo = Alphaleonis . Win32 . Filesystem . FileInfo ;
using Path = Alphaleonis . Win32 . Filesystem . Path ;
2019-07-21 04:40:54 +00:00
namespace Wabbajack
{
public class Compiler
{
2019-09-12 23:44:35 +00:00
private string _mo2DownloadsFolder ;
2019-07-23 04:27:26 +00:00
2019-09-12 23:44:35 +00:00
public Dictionary < string , IEnumerable < IndexedFileMatch > > DirectMatchIndex ;
2019-07-21 04:40:54 +00:00
public string MO2Folder ;
2019-09-12 23:44:35 +00:00
public string MO2Profile ;
public Compiler ( string mo2_folder , Action < string > log_fn )
{
MO2Folder = mo2_folder ;
Log_Fn = log_fn ;
MO2Ini = Path . Combine ( MO2Folder , "ModOrganizer.ini" ) . LoadIniFile ( ) ;
GamePath = ( ( string ) MO2Ini . General . gamePath ) . Replace ( "\\\\" , "\\" ) ;
}
2019-07-21 04:40:54 +00:00
public dynamic MO2Ini { get ; }
public string GamePath { get ; }
2019-09-24 04:20:24 +00:00
public bool ShowReportWhenFinished { get ; set ; } = true ;
2019-08-07 23:06:38 +00:00
public bool IgnoreMissingFiles { get ; set ; }
2019-07-22 22:17:46 +00:00
public string MO2DownloadsFolder
{
2019-07-21 04:40:54 +00:00
get
{
2019-09-03 22:12:39 +00:00
if ( _mo2DownloadsFolder ! = null ) return _mo2DownloadsFolder ;
if ( MO2Ini ! = null )
if ( MO2Ini . Settings ! = null )
if ( MO2Ini . Settings . download_directory ! = null )
return MO2Ini . Settings . download_directory . Replace ( "/" , "\\" ) ;
2019-07-21 04:40:54 +00:00
return Path . Combine ( MO2Folder , "downloads" ) ;
}
2019-09-03 22:12:39 +00:00
set = > _mo2DownloadsFolder = value ;
2019-07-21 04:40:54 +00:00
}
2019-09-12 23:44:35 +00:00
public string MO2ProfileDir = > Path . Combine ( MO2Folder , "profiles" , MO2Profile ) ;
2019-07-21 12:42:29 +00:00
2019-07-21 04:40:54 +00:00
public Action < string > Log_Fn { get ; }
2019-07-21 22:47:17 +00:00
public List < Directive > InstallDirectives { get ; private set ; }
2019-08-05 23:58:42 +00:00
public string NexusKey { get ; private set ; }
internal UserStatus User { get ; private set ; }
2019-07-21 22:47:17 +00:00
public List < Archive > SelectedArchives { get ; private set ; }
2019-07-22 03:36:25 +00:00
public List < RawSourceFile > AllFiles { get ; private set ; }
2019-07-23 04:27:26 +00:00
public ModList ModList { get ; private set ; }
2019-07-26 20:59:14 +00:00
public ConcurrentBag < Directive > ExtraFiles { get ; private set ; }
2019-08-11 22:57:32 +00:00
public Dictionary < string , dynamic > ModInis { get ; private set ; }
2019-07-21 04:40:54 +00:00
2019-09-12 23:44:35 +00:00
public VirtualFileSystem VFS = > VirtualFileSystem . VFS ;
2019-08-20 04:57:08 +00:00
public List < IndexedArchive > IndexedArchives { get ; private set ; }
public Dictionary < string , IEnumerable < VirtualFile > > IndexedFiles { get ; private set ; }
2019-08-09 04:07:23 +00:00
2019-09-12 23:44:35 +00:00
public HashSet < string > SelectedProfiles { get ; set ; } = new HashSet < string > ( ) ;
2019-08-15 04:30:37 +00:00
2019-07-21 04:40:54 +00:00
public void Info ( string msg , params object [ ] args )
{
if ( args . Length > 0 )
2019-09-12 23:44:35 +00:00
msg = string . Format ( msg , args ) ;
2019-07-21 04:40:54 +00:00
Log_Fn ( msg ) ;
}
2019-07-22 22:17:46 +00:00
public void Status ( string msg , params object [ ] args )
{
if ( args . Length > 0 )
2019-09-12 23:44:35 +00:00
msg = string . Format ( msg , args ) ;
2019-07-22 22:17:46 +00:00
WorkQueue . Report ( msg , 0 ) ;
}
2019-07-21 22:47:17 +00:00
private void Error ( string msg , params object [ ] args )
{
if ( args . Length > 0 )
2019-09-12 23:44:35 +00:00
msg = string . Format ( msg , args ) ;
2019-07-21 22:47:17 +00:00
Log_Fn ( msg ) ;
throw new Exception ( msg ) ;
}
2019-09-24 04:20:24 +00:00
public bool Compile ( )
2019-07-21 04:40:54 +00:00
{
2019-09-16 22:47:15 +00:00
VirtualFileSystem . Clean ( ) ;
2019-09-12 23:44:35 +00:00
Info ( "Looking for other profiles" ) ;
2019-09-03 21:17:00 +00:00
var other_profiles_path = Path . Combine ( MO2ProfileDir , "otherprofiles.txt" ) ;
2019-09-07 04:57:49 +00:00
SelectedProfiles = new HashSet < string > ( ) ;
2019-09-12 23:44:35 +00:00
if ( File . Exists ( other_profiles_path ) ) SelectedProfiles = File . ReadAllLines ( other_profiles_path ) . ToHashSet ( ) ;
2019-09-07 04:57:49 +00:00
SelectedProfiles . Add ( MO2Profile ) ;
2019-09-03 21:17:00 +00:00
2019-09-12 23:44:35 +00:00
Info ( "Using Profiles: " + string . Join ( ", " , SelectedProfiles . OrderBy ( p = > p ) ) ) ;
2019-09-03 21:17:00 +00:00
2019-08-20 21:44:32 +00:00
Info ( $"Indexing {MO2Folder}" ) ;
2019-08-20 04:57:08 +00:00
VFS . AddRoot ( MO2Folder ) ;
2019-08-20 21:44:32 +00:00
Info ( $"Indexing {GamePath}" ) ;
2019-08-20 04:57:08 +00:00
VFS . AddRoot ( GamePath ) ;
2019-09-03 22:12:39 +00:00
Info ( $"Indexing {MO2DownloadsFolder}" ) ;
VFS . AddRoot ( MO2DownloadsFolder ) ;
2019-07-21 04:40:54 +00:00
var mo2_files = Directory . EnumerateFiles ( MO2Folder , "*" , SearchOption . AllDirectories )
2019-09-12 23:44:35 +00:00
. Where ( p = > p . FileExists ( ) )
. Select ( p = > new RawSourceFile ( VFS . Lookup ( p ) ) { Path = p . RelativeTo ( MO2Folder ) } ) ;
2019-07-21 04:40:54 +00:00
var game_files = Directory . EnumerateFiles ( GamePath , "*" , SearchOption . AllDirectories )
2019-09-12 23:44:35 +00:00
. Where ( p = > p . FileExists ( ) )
. Select ( p = > new RawSourceFile ( VFS . Lookup ( p ) )
{ Path = Path . Combine ( Consts . GameFolderFilesDir , p . RelativeTo ( GamePath ) ) } ) ;
2019-07-21 04:40:54 +00:00
2019-09-12 23:44:35 +00:00
var loot_path = Path . Combine ( Environment . GetFolderPath ( Environment . SpecialFolder . LocalApplicationData ) ,
"LOOT" ) ;
2019-08-24 23:59:22 +00:00
2019-09-24 04:34:21 +00:00
// TODO: make this generic so we can add more paths
IEnumerable < RawSourceFile > loot_files = new List < RawSourceFile > ( ) ;
if ( Directory . Exists ( loot_path ) )
{
Info ( $"Indexing {loot_path}" ) ;
VFS . AddRoot ( loot_path ) ;
loot_files = Directory . EnumerateFiles ( loot_path , "userlist.yaml" , SearchOption . AllDirectories )
. Where ( p = > p . FileExists ( ) )
. Select ( p = > new RawSourceFile ( VFS . Lookup ( p ) )
{ Path = Path . Combine ( Consts . LOOTFolderFilesDir , p . RelativeTo ( loot_path ) ) } ) ;
}
2019-08-24 23:59:22 +00:00
2019-08-20 21:44:32 +00:00
2019-09-12 23:44:35 +00:00
Info ( "Indexing Archives" ) ;
2019-08-20 04:57:08 +00:00
IndexedArchives = Directory . EnumerateFiles ( MO2DownloadsFolder )
2019-09-12 23:44:35 +00:00
. Where ( f = > File . Exists ( f + ".meta" ) )
. Select ( f = > new IndexedArchive
{
File = VFS . Lookup ( f ) ,
Name = Path . GetFileName ( f ) ,
IniData = ( f + ".meta" ) . LoadIniFile ( ) ,
Meta = File . ReadAllText ( f + ".meta" )
} )
. ToList ( ) ;
Info ( "Indexing Files" ) ;
2019-09-13 00:18:50 +00:00
var grouped = VFS . GroupedByArchive ( ) ;
IndexedFiles = IndexedArchives . Select ( f = >
2019-09-12 23:44:35 +00:00
{
2019-09-13 00:18:50 +00:00
if ( grouped . TryGetValue ( f . File , out var result ) )
return result ;
return new List < VirtualFile > ( ) ;
2019-09-12 23:44:35 +00:00
} )
. SelectMany ( fs = > fs )
. Concat ( IndexedArchives . Select ( f = > f . File ) )
. OrderByDescending ( f = > f . TopLevelArchive . LastModified )
. GroupBy ( f = > f . Hash )
. ToDictionary ( f = > f . Key , f = > f . AsEnumerable ( ) ) ;
2019-08-15 04:30:37 +00:00
2019-08-20 04:57:08 +00:00
Info ( "Searching for mod files" ) ;
2019-08-15 04:30:37 +00:00
2019-08-24 23:59:22 +00:00
AllFiles = mo2_files . Concat ( game_files )
2019-09-12 23:44:35 +00:00
. Concat ( loot_files )
2019-09-23 21:37:10 +00:00
. DistinctBy ( f = > f . Path )
2019-09-12 23:44:35 +00:00
. ToList ( ) ;
2019-08-15 04:30:37 +00:00
2019-07-22 03:36:25 +00:00
Info ( "Found {0} files to build into mod list" , AllFiles . Count ) ;
2019-07-21 04:40:54 +00:00
2019-07-26 20:59:14 +00:00
ExtraFiles = new ConcurrentBag < Directive > ( ) ;
2019-08-11 22:57:32 +00:00
ModInis = Directory . EnumerateDirectories ( Path . Combine ( MO2Folder , "mods" ) )
2019-09-12 23:44:35 +00:00
. Select ( f = >
{
var mod_name = Path . GetFileName ( f ) ;
var meta_path = Path . Combine ( f , "meta.ini" ) ;
if ( File . Exists ( meta_path ) )
return ( mod_name , meta_path . LoadIniFile ( ) ) ;
return ( null , null ) ;
} )
. Where ( f = > f . Item2 ! = null )
. ToDictionary ( f = > f . Item1 , f = > f . Item2 ) ;
2019-08-11 22:57:32 +00:00
2019-08-20 04:57:08 +00:00
var stack = MakeStack ( ) ;
2019-07-21 04:40:54 +00:00
2019-08-25 03:46:32 +00:00
2019-07-22 22:17:46 +00:00
Info ( "Running Compilation Stack" ) ;
var results = AllFiles . PMap ( f = > RunStack ( stack , f ) ) . ToList ( ) ;
2019-07-21 04:40:54 +00:00
2019-07-26 20:59:14 +00:00
// Add the extra files that were generated by the stack
Info ( $"Adding {ExtraFiles.Count} that were generated by the stack" ) ;
results = results . Concat ( ExtraFiles ) . ToList ( ) ;
2019-07-21 04:40:54 +00:00
var nomatch = results . OfType < NoMatch > ( ) ;
Info ( "No match for {0} files" , nomatch . Count ( ) ) ;
foreach ( var file in nomatch )
Info ( " {0}" , file . To ) ;
2019-07-26 20:59:14 +00:00
if ( nomatch . Count ( ) > 0 )
2019-08-02 22:31:13 +00:00
{
2019-08-07 23:06:38 +00:00
if ( IgnoreMissingFiles )
{
Info ( "Continuing even though files were missing at the request of the user." ) ;
}
2019-09-12 23:44:35 +00:00
else
{
2019-08-07 23:06:38 +00:00
Info ( "Exiting due to no way to compile these files" ) ;
2019-09-24 04:20:24 +00:00
return false ;
2019-08-07 23:06:38 +00:00
}
2019-08-02 22:31:13 +00:00
}
2019-07-21 04:40:54 +00:00
2019-07-21 22:47:17 +00:00
InstallDirectives = results . Where ( i = > ! ( i is IgnoredDirectly ) ) . ToList ( ) ;
2019-08-09 21:13:02 +00:00
Info ( "Getting nexus api_key please click authorize if a browser window appears" ) ;
2019-09-24 04:20:24 +00:00
if ( IndexedArchives . OfType < NexusMod > ( ) . Any ( ) )
{
NexusKey = GetNexusAPIKey ( ) ;
User = GetUserStatus ( NexusKey ) ;
if ( ! User . is_premium ) Info ( $"User {User.name} is not a premium Nexus user, cannot continue" ) ;
2019-09-12 23:44:35 +00:00
2019-09-24 04:20:24 +00:00
}
2019-09-12 23:44:35 +00:00
2019-09-23 21:37:10 +00:00
zEditIntegration . VerifyMerges ( this ) ;
2019-08-05 23:58:42 +00:00
2019-07-21 22:47:17 +00:00
GatherArchives ( ) ;
2019-07-22 03:36:25 +00:00
BuildPatches ( ) ;
2019-07-23 04:27:26 +00:00
2019-09-12 23:44:35 +00:00
ModList = new ModList
2019-07-23 04:27:26 +00:00
{
Archives = SelectedArchives ,
2019-08-02 23:04:04 +00:00
Directives = InstallDirectives ,
Name = MO2Profile
2019-07-23 04:27:26 +00:00
} ;
2019-08-30 23:57:56 +00:00
GenerateReport ( ) ;
2019-07-22 22:17:46 +00:00
PatchExecutable ( ) ;
2019-09-12 23:44:35 +00:00
2019-07-23 04:27:26 +00:00
ResetMembers ( ) ;
2019-08-30 23:57:56 +00:00
ShowReport ( ) ;
2019-07-22 22:17:46 +00:00
Info ( "Done Building Modpack" ) ;
2019-09-24 04:20:24 +00:00
return true ;
2019-07-21 04:40:54 +00:00
}
2019-09-12 23:44:35 +00:00
2019-08-30 23:57:56 +00:00
private void ShowReport ( )
{
2019-09-24 04:20:24 +00:00
if ( ! ShowReportWhenFinished ) return ;
2019-08-30 23:57:56 +00:00
var file = Path . GetTempFileName ( ) + ".html" ;
File . WriteAllText ( file , ModList . ReportHTML ) ;
Process . Start ( file ) ;
}
private void GenerateReport ( )
{
using ( var fs = File . OpenWrite ( $"{ModList.Name}.md" ) )
{
fs . SetLength ( 0 ) ;
using ( var reporter = new ReportBuilder ( fs ) )
2019-09-12 23:44:35 +00:00
{
2019-08-30 23:57:56 +00:00
reporter . Build ( ModList ) ;
2019-09-12 23:44:35 +00:00
}
2019-08-30 23:57:56 +00:00
}
2019-09-12 23:44:35 +00:00
2019-08-30 23:57:56 +00:00
ModList . ReportHTML = CommonMarkConverter . Convert ( File . ReadAllText ( $"{ModList.Name}.md" ) ) ;
}
2019-07-21 04:40:54 +00:00
2019-07-23 04:27:26 +00:00
/// <summary>
2019-09-12 23:44:35 +00:00
/// Clear references to lists that hold a lot of data.
2019-07-23 04:27:26 +00:00
/// </summary>
private void ResetMembers ( )
{
AllFiles = null ;
InstallDirectives = null ;
SelectedArchives = null ;
2019-07-26 20:59:14 +00:00
ExtraFiles = null ;
2019-07-23 04:27:26 +00:00
}
2019-07-22 03:36:25 +00:00
/// <summary>
2019-09-12 23:44:35 +00:00
/// Fills in the Patch fields in files that require them
2019-07-22 03:36:25 +00:00
/// </summary>
private void BuildPatches ( )
{
2019-09-12 23:44:35 +00:00
Info ( "Gathering patch files" ) ;
2019-07-22 03:36:25 +00:00
var groups = InstallDirectives . OfType < PatchedFromArchive > ( )
2019-09-12 23:44:35 +00:00
. Where ( p = > p . Patch = = null )
. GroupBy ( p = > p . ArchiveHashPath [ 0 ] )
. ToList ( ) ;
2019-07-22 03:36:25 +00:00
Info ( "Patching building patches from {0} archives" , groups . Count ) ;
var absolute_paths = AllFiles . ToDictionary ( e = > e . Path , e = > e . AbsolutePath ) ;
2019-07-22 22:17:46 +00:00
groups . PMap ( group = > BuildArchivePatches ( group . Key , group , absolute_paths ) ) ;
2019-07-22 03:36:25 +00:00
if ( InstallDirectives . OfType < PatchedFromArchive > ( ) . FirstOrDefault ( f = > f . Patch = = null ) ! = null )
Error ( "Missing patches after generation, this should not happen" ) ;
}
2019-09-12 23:44:35 +00:00
private void BuildArchivePatches ( string archive_sha , IEnumerable < PatchedFromArchive > group ,
Dictionary < string , string > absolute_paths )
2019-07-22 03:36:25 +00:00
{
2019-08-20 04:57:08 +00:00
var archive = VFS . HashIndex [ archive_sha ] ;
using ( var files = VFS . StageWith ( group . Select ( g = > VFS . FileForArchiveHashPath ( g . ArchiveHashPath ) ) ) )
2019-07-22 03:36:25 +00:00
{
2019-09-12 23:44:35 +00:00
var by_path = files . GroupBy ( f = > string . Join ( "|" , f . Paths . Skip ( 1 ) ) )
. ToDictionary ( f = > f . Key , f = > f . First ( ) ) ;
2019-08-20 04:57:08 +00:00
// Now Create the patches
group . PMap ( entry = >
2019-07-22 03:36:25 +00:00
{
2019-08-20 04:57:08 +00:00
Info ( "Patching {0}" , entry . To ) ;
using ( var origin = by_path [ string . Join ( "|" , entry . ArchiveHashPath . Skip ( 1 ) ) ] . OpenRead ( ) )
using ( var output = new MemoryStream ( ) )
{
var a = origin . ReadAll ( ) ;
var b = LoadDataForTo ( entry . To , absolute_paths ) ;
2019-09-12 23:44:35 +00:00
Utils . CreatePatch ( a , b , output ) ;
2019-09-13 00:18:50 +00:00
entry . Patch = output . ToArray ( ) ;
2019-08-20 04:57:08 +00:00
Info ( $"Patch size {entry.Patch.Length} for {entry.To}" ) ;
}
} ) ;
}
2019-07-22 03:36:25 +00:00
}
2019-07-26 20:59:14 +00:00
private byte [ ] LoadDataForTo ( string to , Dictionary < string , string > absolute_paths )
{
if ( absolute_paths . TryGetValue ( to , out var absolute ) )
return File . ReadAllBytes ( absolute ) ;
if ( to . StartsWith ( Consts . BSACreationDir ) )
{
var bsa_id = to . Split ( '\\' ) [ 1 ] ;
var bsa = InstallDirectives . OfType < CreateBSA > ( ) . First ( b = > b . TempID = = bsa_id ) ;
2019-07-30 03:32:52 +00:00
using ( var a = new BSAReader ( Path . Combine ( MO2Folder , bsa . To ) ) )
2019-07-26 20:59:14 +00:00
{
2019-07-30 03:32:52 +00:00
var file = a . Files . First ( e = > e . Path = = Path . Combine ( to . Split ( '\\' ) . Skip ( 2 ) . ToArray ( ) ) ) ;
return file . GetData ( ) ;
2019-07-26 20:59:14 +00:00
}
}
2019-09-12 23:44:35 +00:00
2019-07-26 20:59:14 +00:00
Error ( $"Couldn't load data for {to}" ) ;
return null ;
}
2019-07-21 22:47:17 +00:00
private void GatherArchives ( )
{
2019-09-12 23:44:35 +00:00
Info ( "Building a list of archives based on the files required" ) ;
2019-07-21 22:47:17 +00:00
var shas = InstallDirectives . OfType < FromArchive > ( )
2019-09-12 23:44:35 +00:00
. Select ( a = > a . ArchiveHashPath [ 0 ] )
. Distinct ( ) ;
2019-07-21 22:47:17 +00:00
2019-08-20 04:57:08 +00:00
var archives = IndexedArchives . OrderByDescending ( f = > f . File . LastModified )
2019-09-12 23:44:35 +00:00
. GroupBy ( f = > f . File . Hash )
. ToDictionary ( f = > f . Key , f = > f . First ( ) ) ;
2019-08-20 04:57:08 +00:00
2019-08-05 23:58:42 +00:00
SelectedArchives = shas . PMap ( sha = > ResolveArchive ( sha , archives ) ) ;
2019-07-21 22:47:17 +00:00
}
2019-08-20 04:57:08 +00:00
private Archive ResolveArchive ( string sha , IDictionary < string , IndexedArchive > archives )
2019-07-21 22:47:17 +00:00
{
2019-07-22 22:17:46 +00:00
if ( archives . TryGetValue ( sha , out var found ) )
2019-07-21 22:47:17 +00:00
{
if ( found . IniData = = null )
2019-09-12 23:44:35 +00:00
Error (
"No download metadata found for {0}, please use MO2 to query info or add a .meta file and try again." ,
found . Name ) ;
2019-07-21 22:47:17 +00:00
var general = found . IniData . General ;
if ( general = = null )
2019-09-12 23:44:35 +00:00
Error (
"No General section in mod metadata found for {0}, please use MO2 to query info or add the info and try again." ,
found . Name ) ;
2019-07-21 22:47:17 +00:00
Archive result ;
2019-08-25 03:46:32 +00:00
if ( general . directURL ! = null & & general . directURL . StartsWith ( "https://drive.google.com" ) )
2019-07-26 20:59:14 +00:00
{
var regex = new Regex ( "((?<=id=)[a-zA-Z0-9_-]*)|(?<=\\/file\\/d\\/)[a-zA-Z0-9_-]*" ) ;
var match = regex . Match ( general . directURL ) ;
2019-09-12 23:44:35 +00:00
result = new GoogleDriveMod
2019-07-26 20:59:14 +00:00
{
Id = match . ToString ( )
} ;
}
2019-08-04 22:08:03 +00:00
else if ( general . directURL ! = null & & general . directURL . StartsWith ( Consts . MegaPrefix ) )
{
2019-09-12 23:44:35 +00:00
result = new MEGAArchive
2019-08-04 22:08:03 +00:00
{
URL = general . directURL
} ;
}
2019-07-24 04:31:08 +00:00
else if ( general . directURL ! = null & & general . directURL . StartsWith ( "https://www.dropbox.com/" ) )
{
2019-09-12 23:44:35 +00:00
var uri = new UriBuilder ( ( string ) general . directURL ) ;
2019-07-24 04:31:08 +00:00
var query = HttpUtility . ParseQueryString ( uri . Query ) ;
if ( query . GetValues ( "dl" ) . Count ( ) > 0 )
query . Remove ( "dl" ) ;
query . Set ( "dl" , "1" ) ;
uri . Query = query . ToString ( ) ;
2019-09-12 23:44:35 +00:00
result = new DirectURLArchive
2019-07-24 04:31:08 +00:00
{
URL = uri . ToString ( )
} ;
}
2019-09-12 23:44:35 +00:00
else if ( general . directURL ! = null & &
general . directURL . StartsWith ( "https://www.moddb.com/downloads/start" ) )
2019-07-24 04:31:08 +00:00
{
2019-09-12 23:44:35 +00:00
result = new MODDBArchive
2019-07-24 04:31:08 +00:00
{
URL = general . directURL
} ;
}
2019-08-02 22:31:13 +00:00
else if ( general . directURL ! = null & & general . directURL . StartsWith ( "http://www.mediafire.com/file/" ) )
{
Error ( "Mediafire links are not currently supported" ) ;
return null ;
/ * result = new MediaFireArchive ( )
{
URL = general . directURL
} ; * /
}
2019-07-21 22:47:17 +00:00
else if ( general . directURL ! = null )
{
2019-09-12 23:44:35 +00:00
var tmp = new DirectURLArchive
2019-07-21 22:47:17 +00:00
{
URL = general . directURL
} ;
2019-08-02 22:31:13 +00:00
if ( general . directURLHeaders ! = null )
{
tmp . Headers = new List < string > ( ) ;
tmp . Headers . AddRange ( general . directURLHeaders . Split ( '|' ) ) ;
}
2019-09-12 23:44:35 +00:00
2019-08-02 22:31:13 +00:00
result = tmp ;
2019-07-21 22:47:17 +00:00
}
2019-08-25 03:46:32 +00:00
else if ( general . modID ! = null & & general . fileID ! = null & & general . gameName ! = null )
{
2019-09-12 23:44:35 +00:00
var nm = new NexusMod
2019-08-25 03:46:32 +00:00
{
GameName = general . gameName ,
FileID = general . fileID ,
2019-08-27 00:19:23 +00:00
ModID = general . modID ,
Version = general . version ? ? "0.0.0.0"
2019-08-25 03:46:32 +00:00
} ;
2019-09-12 23:44:35 +00:00
var info = GetModInfo ( nm , NexusKey ) ;
2019-08-30 23:57:56 +00:00
nm . Author = info . author ;
nm . UploadedBy = info . uploaded_by ;
nm . UploaderProfile = info . uploaded_users_profile_url ;
result = nm ;
2019-08-25 03:46:32 +00:00
}
2019-09-24 04:20:24 +00:00
else if ( general . manualURL ! = null )
{
result = new ManualArchive
{
URL = general . manualURL ,
Notes = general . manualNotes ,
} ;
}
2019-07-21 22:47:17 +00:00
else
{
2019-09-12 15:32:13 +00:00
Error ( "No way to handle archive {0} but it's required by the modlist" , found . Name ) ;
2019-07-21 22:47:17 +00:00
return null ;
}
result . Name = found . Name ;
2019-08-20 04:57:08 +00:00
result . Hash = found . File . Hash ;
2019-07-21 22:47:17 +00:00
result . Meta = found . Meta ;
2019-09-10 13:02:25 +00:00
result . Size = found . File . Size ;
2019-07-21 22:47:17 +00:00
2019-09-24 04:20:24 +00:00
if ( result is ManualArchive ) return result ;
2019-08-30 04:24:31 +00:00
Info ( $"Checking link for {found.Name}" ) ;
2019-09-03 22:12:39 +00:00
var installer = new Installer ( null , "" , Utils . Log ) ;
2019-08-30 04:24:31 +00:00
installer . NexusAPIKey = NexusKey ;
if ( ! installer . DownloadArchive ( result , false ) )
2019-09-12 23:44:35 +00:00
Error (
$"Unable to resolve link for {found.Name}. If this is hosted on the nexus the file may have been removed." ) ;
2019-08-30 04:24:31 +00:00
2019-07-21 22:47:17 +00:00
return result ;
}
2019-09-12 23:44:35 +00:00
2019-07-21 22:47:17 +00:00
Error ( "No match found for Archive sha: {0} this shouldn't happen" , sha ) ;
return null ;
}
2019-07-21 04:40:54 +00:00
private Directive RunStack ( IEnumerable < Func < RawSourceFile , Directive > > stack , RawSourceFile source )
{
2019-07-22 22:17:46 +00:00
Status ( "Compiling {0}" , source . Path ) ;
2019-08-15 04:30:37 +00:00
foreach ( var f in stack )
{
var result = f ( source ) ;
if ( result ! = null ) return result ;
}
2019-09-12 23:44:35 +00:00
2019-08-15 04:30:37 +00:00
throw new InvalidDataException ( "Data fell out of the compilation stack" ) ;
2019-07-21 04:40:54 +00:00
}
/// <summary>
2019-09-12 23:44:35 +00:00
/// Creates a execution stack. The stack should be passed into Run stack. Each function
/// in this stack will be run in-order and the first to return a non-null result will have its
/// result included into the pack
2019-07-21 04:40:54 +00:00
/// </summary>
/// <returns></returns>
2019-08-20 04:57:08 +00:00
private IEnumerable < Func < RawSourceFile , Directive > > MakeStack ( )
2019-07-21 04:40:54 +00:00
{
Info ( "Generating compilation stack" ) ;
2019-09-03 22:12:39 +00:00
return new List < Func < RawSourceFile , Directive > >
2019-07-21 04:40:54 +00:00
{
IgnoreStartsWith ( "logs\\" ) ,
2019-08-22 22:05:16 +00:00
IncludeRegex ( "^downloads\\\\.*\\.meta" ) ,
2019-07-21 04:40:54 +00:00
IgnoreStartsWith ( "downloads\\" ) ,
IgnoreStartsWith ( "webcache\\" ) ,
2019-07-26 20:59:14 +00:00
IgnoreStartsWith ( "overwrite\\" ) ,
2019-08-15 04:30:37 +00:00
IgnorePathContains ( "temporary_logs" ) ,
2019-08-24 04:47:52 +00:00
IgnorePathContains ( "GPUCache" ) ,
IgnorePathContains ( "SSEEdit Cache" ) ,
2019-07-21 04:40:54 +00:00
IgnoreEndsWith ( ".pyc" ) ,
2019-08-12 21:00:22 +00:00
IgnoreEndsWith ( ".log" ) ,
2019-07-21 12:42:29 +00:00
IgnoreOtherProfiles ( ) ,
2019-07-23 04:27:26 +00:00
IgnoreDisabledMods ( ) ,
2019-07-21 12:42:29 +00:00
IncludeThisProfile ( ) ,
2019-07-21 04:40:54 +00:00
// Ignore the ModOrganizer.ini file it contains info created by MO2 on startup
2019-09-02 22:36:57 +00:00
IncludeStubbedConfigFiles ( ) ,
2019-08-24 23:59:22 +00:00
IncludeLootFiles ( ) ,
2019-07-24 04:31:08 +00:00
IgnoreStartsWith ( Path . Combine ( Consts . GameFolderFilesDir , "Data" ) ) ,
IgnoreStartsWith ( Path . Combine ( Consts . GameFolderFilesDir , "Papyrus Compiler" ) ) ,
IgnoreStartsWith ( Path . Combine ( Consts . GameFolderFilesDir , "Skyrim" ) ) ,
2019-07-21 04:40:54 +00:00
IgnoreRegex ( Consts . GameFolderFilesDir + "\\\\.*\\.bsa" ) ,
2019-07-21 12:42:29 +00:00
IncludeModIniData ( ) ,
2019-07-21 04:40:54 +00:00
DirectMatch ( ) ,
2019-09-20 22:49:32 +00:00
IncludeTaggedFiles ( Consts . WABBAJACK_INCLUDE ) ,
2019-08-20 04:57:08 +00:00
DeconstructBSAs ( ) , // Deconstruct BSAs before building patches so we don't generate massive patch files
2019-07-21 22:47:17 +00:00
IncludePatches ( ) ,
2019-08-10 16:03:51 +00:00
IncludeDummyESPs ( ) ,
2019-07-21 12:42:29 +00:00
2019-07-26 20:59:14 +00:00
2019-07-21 12:42:29 +00:00
// If we have no match at this point for a game folder file, skip them, we can't do anything about them
IgnoreGameFiles ( ) ,
2019-07-26 20:59:14 +00:00
// There are some types of files that will error the compilation, because tehy're created on-the-fly via tools
// so if we don't have a match by this point, just drop them.
IgnoreEndsWith ( ".ini" ) ,
IgnoreEndsWith ( ".html" ) ,
IgnoreEndsWith ( ".txt" ) ,
// Don't know why, but this seems to get copied around a bit
IgnoreEndsWith ( "HavokBehaviorPostProcess.exe" ) ,
2019-08-03 17:37:32 +00:00
// Theme file MO2 downloads somehow
IgnoreEndsWith ( "splash.png" ) ,
2019-08-25 03:46:32 +00:00
2019-09-16 20:40:06 +00:00
IgnoreEndsWith ( ".bin" ) ,
IgnoreEndsWith ( ".refcache" ) ,
2019-09-13 00:18:50 +00:00
IgnoreWabbajackInstallCruft ( ) ,
2019-08-25 03:46:32 +00:00
PatchStockESMs ( ) ,
2019-09-04 20:49:13 +00:00
IncludeAllConfigs ( ) ,
2019-09-20 22:49:32 +00:00
IncludeTaggedFiles ( Consts . WABBAJACK_NOMATCH_INCLUDE ) ,
2019-09-23 21:37:10 +00:00
zEditIntegration . IncludezEditPatches ( this ) ,
2019-09-04 20:49:13 +00:00
2019-07-26 20:59:14 +00:00
DropAll ( )
} ;
}
2019-09-13 00:18:50 +00:00
private Func < RawSourceFile , Directive > IgnoreWabbajackInstallCruft ( )
{
2019-09-14 04:35:42 +00:00
var cruft_files = new HashSet < string >
{
"7z.dll" , "7z.exe" , "vfs_staged_files\\" , "nexus.key_cache" , "patch_cache\\" ,
Consts . NexusCacheDirectory + "\\"
} ;
2019-09-13 00:18:50 +00:00
return source = >
{
if ( ! cruft_files . Any ( f = > source . Path . StartsWith ( f ) ) ) return null ;
var result = source . EvolveTo < IgnoredDirectly > ( ) ;
result . Reason = "Wabbajack Cruft file" ;
return result ;
} ;
}
2019-09-04 20:49:13 +00:00
private Func < RawSourceFile , Directive > IncludeAllConfigs ( )
{
return source = >
{
if ( ! Consts . ConfigFileExtensions . Contains ( Path . GetExtension ( source . Path ) ) ) return null ;
var result = source . EvolveTo < InlineFile > ( ) ;
result . SourceData = File . ReadAllBytes ( source . AbsolutePath ) . ToBase64 ( ) ;
return result ;
} ;
}
2019-08-25 03:46:32 +00:00
private Func < RawSourceFile , Directive > PatchStockESMs ( )
{
return source = >
{
2019-09-12 23:44:35 +00:00
var filename = Path . GetFileName ( source . Path ) ;
var game_file = Path . Combine ( GamePath , "Data" , filename ) ;
2019-08-25 03:46:32 +00:00
if ( Consts . GameESMs . Contains ( filename ) & & source . Path . StartsWith ( "mods\\" ) & & File . Exists ( game_file ) )
{
2019-09-12 23:44:35 +00:00
Info (
$"A ESM named {filename} was found in a mod that shares a name with a core game ESMs, it is assumed this is a cleaned ESM and it will be binary patched." ) ;
2019-08-25 03:46:32 +00:00
var result = source . EvolveTo < CleanedESM > ( ) ;
result . SourceESMHash = VFS . Lookup ( game_file ) . Hash ;
Status ( $"Generating patch of {filename}" ) ;
2019-09-12 23:44:35 +00:00
using ( var ms = new MemoryStream ( ) )
{
Utils . CreatePatch ( File . ReadAllBytes ( game_file ) , File . ReadAllBytes ( source . AbsolutePath ) , ms ) ;
2019-08-25 03:46:32 +00:00
result . SourceData = ms . ToArray ( ) . ToBase64 ( ) ;
}
2019-09-12 23:44:35 +00:00
2019-08-25 03:46:32 +00:00
Info ( $"Generated a {result.SourceData.Length} byte patch for {filename}" ) ;
return result ;
}
2019-09-12 23:44:35 +00:00
2019-08-25 03:46:32 +00:00
return null ;
} ;
}
2019-08-24 23:59:22 +00:00
private Func < RawSourceFile , Directive > IncludeLootFiles ( )
{
var prefix = Consts . LOOTFolderFilesDir + "\\" ;
return source = >
{
if ( source . Path . StartsWith ( prefix ) )
{
var result = source . EvolveTo < InlineFile > ( ) ;
result . SourceData = File . ReadAllBytes ( source . AbsolutePath ) . ToBase64 ( ) ;
return result ;
}
2019-09-12 23:44:35 +00:00
2019-08-24 23:59:22 +00:00
return null ;
} ;
}
2019-09-02 22:36:57 +00:00
private Func < RawSourceFile , Directive > IncludeStubbedConfigFiles ( )
2019-08-24 23:20:54 +00:00
{
return source = >
{
2019-09-02 22:36:57 +00:00
if ( Consts . ConfigFileExtensions . Contains ( Path . GetExtension ( source . Path ) ) )
return RemapFile ( source , GamePath ) ;
2019-08-24 23:20:54 +00:00
return null ;
} ;
}
2019-09-02 22:36:57 +00:00
private Directive RemapFile ( RawSourceFile source , string gamePath )
2019-08-24 23:20:54 +00:00
{
var data = File . ReadAllText ( source . AbsolutePath ) ;
2019-09-02 22:36:57 +00:00
var original_data = data ;
2019-08-24 23:20:54 +00:00
data = data . Replace ( GamePath , Consts . GAME_PATH_MAGIC_BACK ) ;
data = data . Replace ( GamePath . Replace ( "\\" , "\\\\" ) , Consts . GAME_PATH_MAGIC_DOUBLE_BACK ) ;
data = data . Replace ( GamePath . Replace ( "\\" , "/" ) , Consts . GAME_PATH_MAGIC_FORWARD ) ;
data = data . Replace ( MO2Folder , Consts . MO2_PATH_MAGIC_BACK ) ;
data = data . Replace ( MO2Folder . Replace ( "\\" , "\\\\" ) , Consts . MO2_PATH_MAGIC_DOUBLE_BACK ) ;
data = data . Replace ( MO2Folder . Replace ( "\\" , "/" ) , Consts . MO2_PATH_MAGIC_FORWARD ) ;
2019-09-03 22:12:39 +00:00
data = data . Replace ( MO2DownloadsFolder , Consts . DOWNLOAD_PATH_MAGIC_BACK ) ;
data = data . Replace ( MO2DownloadsFolder . Replace ( "\\" , "\\\\" ) , Consts . DOWNLOAD_PATH_MAGIC_DOUBLE_BACK ) ;
data = data . Replace ( MO2DownloadsFolder . Replace ( "\\" , "/" ) , Consts . DOWNLOAD_PATH_MAGIC_FORWARD ) ;
2019-09-02 22:36:57 +00:00
if ( data = = original_data )
return null ;
2019-08-24 23:20:54 +00:00
var result = source . EvolveTo < RemappedInlineFile > ( ) ;
result . SourceData = Encoding . UTF8 . GetBytes ( data ) . ToBase64 ( ) ;
return result ;
}
2019-08-15 04:30:37 +00:00
private Func < RawSourceFile , Directive > IgnorePathContains ( string v )
{
v = $"\\{v.Trim('\\')}\\" ;
var reason = $"Ignored because path contains {v}" ;
return source = >
{
if ( source . Path . Contains ( v ) )
{
var result = source . EvolveTo < IgnoredDirectly > ( ) ;
result . Reason = reason ;
return result ;
}
2019-09-12 23:44:35 +00:00
2019-08-15 04:30:37 +00:00
return null ;
} ;
}
2019-07-26 20:59:14 +00:00
2019-08-11 22:57:32 +00:00
/// <summary>
2019-09-12 23:44:35 +00:00
/// If a user includes WABBAJACK_INCLUDE directly in the notes or comments of a mod, the contents of that
/// mod will be inlined into the installer. USE WISELY.
2019-08-11 22:57:32 +00:00
/// </summary>
/// <returns></returns>
2019-09-20 22:49:32 +00:00
private Func < RawSourceFile , Directive > IncludeTaggedFiles ( string tag )
2019-08-11 22:57:32 +00:00
{
2019-09-12 23:44:35 +00:00
var include_directly = ModInis . Where ( kv = >
{
2019-08-11 22:57:32 +00:00
var general = kv . Value . General ;
2019-09-20 22:49:32 +00:00
if ( general . notes ! = null & & general . notes . Contains ( tag ) )
2019-08-11 22:57:32 +00:00
return true ;
2019-09-20 22:49:32 +00:00
if ( general . comments ! = null & & general . comments . Contains ( tag ) )
2019-08-11 22:57:32 +00:00
return true ;
return false ;
2019-09-12 23:44:35 +00:00
} ) . Select ( kv = > $"mods\\{kv.Key}\\" ) ;
2019-08-11 22:57:32 +00:00
return source = >
{
if ( source . Path . StartsWith ( "mods" ) )
foreach ( var modpath in include_directly )
if ( source . Path . StartsWith ( modpath ) )
{
var result = source . EvolveTo < InlineFile > ( ) ;
result . SourceData = File . ReadAllBytes ( source . AbsolutePath ) . ToBase64 ( ) ;
return result ;
}
2019-09-12 23:44:35 +00:00
2019-08-11 22:57:32 +00:00
return null ;
} ;
}
2019-08-10 16:03:51 +00:00
/// <summary>
2019-09-12 23:44:35 +00:00
/// Some tools like the Cathedral Asset Optimizer will create dummy ESPs whos only existance is to make
/// sure a BSA with the same name is loaded. We don't have a good way to detect these, but if an ESP is
/// less than 100 bytes in size and shares a name with a BSA it's a pretty good chance that it's a dummy
/// and the contents are generated.
2019-08-10 16:03:51 +00:00
/// </summary>
/// <returns></returns>
private Func < RawSourceFile , Directive > IncludeDummyESPs ( )
{
return source = >
{
2019-09-22 13:56:03 +00:00
if ( Path . GetExtension ( source . AbsolutePath ) = = ".esp" | | Path . GetExtension ( source . AbsolutePath ) = = ".esm" )
2019-08-10 16:03:51 +00:00
{
2019-09-12 23:44:35 +00:00
var bsa = Path . Combine ( Path . GetDirectoryName ( source . AbsolutePath ) ,
Path . GetFileNameWithoutExtension ( source . AbsolutePath ) + ".bsa" ) ;
var bsa_textures = Path . Combine ( Path . GetDirectoryName ( source . AbsolutePath ) ,
Path . GetFileNameWithoutExtension ( source . AbsolutePath ) + " - Textures.bsa" ) ;
2019-08-10 16:03:51 +00:00
var esp_size = new FileInfo ( source . AbsolutePath ) . Length ;
2019-09-22 13:56:03 +00:00
if ( esp_size < = 250 & & ( File . Exists ( bsa ) | | File . Exists ( bsa_textures ) ) )
2019-08-10 16:03:51 +00:00
{
var inline = source . EvolveTo < InlineFile > ( ) ;
inline . SourceData = File . ReadAllBytes ( source . AbsolutePath ) . ToBase64 ( ) ;
return inline ;
}
}
return null ;
} ;
}
2019-07-26 20:59:14 +00:00
/// <summary>
2019-09-12 23:44:35 +00:00
/// This function will search for a way to create a BSA in the installed mod list by assembling it from files
/// found in archives. To do this we hash all the files in side the BSA then try to find matches and patches for
/// all of the files.
2019-07-26 20:59:14 +00:00
/// </summary>
/// <returns></returns>
2019-08-20 04:57:08 +00:00
private Func < RawSourceFile , Directive > DeconstructBSAs ( )
2019-07-26 20:59:14 +00:00
{
2019-09-12 23:44:35 +00:00
var include_directly = ModInis . Where ( kv = >
{
2019-08-15 04:30:37 +00:00
var general = kv . Value . General ;
if ( general . notes ! = null & & general . notes . Contains ( Consts . WABBAJACK_INCLUDE ) )
return true ;
if ( general . comments ! = null & & general . comments . Contains ( Consts . WABBAJACK_INCLUDE ) )
return true ;
return false ;
} ) . Select ( kv = > $"mods\\{kv.Key}\\" ) ;
2019-09-12 23:44:35 +00:00
var microstack = new List < Func < RawSourceFile , Directive > >
2019-07-26 20:59:14 +00:00
{
DirectMatch ( ) ,
IncludePatches ( ) ,
2019-07-21 04:40:54 +00:00
DropAll ( )
} ;
2019-07-26 20:59:14 +00:00
2019-09-12 23:44:35 +00:00
var microstack_with_include = new List < Func < RawSourceFile , Directive > >
2019-08-15 04:30:37 +00:00
{
DirectMatch ( ) ,
IncludePatches ( ) ,
IncludeALL ( )
} ;
2019-07-26 20:59:14 +00:00
return source = >
{
if ( ! Consts . SupportedBSAs . Contains ( Path . GetExtension ( source . Path ) ) ) return null ;
2019-09-12 23:44:35 +00:00
var default_include = false ;
2019-08-15 04:30:37 +00:00
if ( source . Path . StartsWith ( "mods" ) )
foreach ( var modpath in include_directly )
if ( source . Path . StartsWith ( modpath ) )
{
default_include = true ;
break ;
}
2019-08-20 04:57:08 +00:00
var source_files = source . File . FileInArchive ;
2019-07-26 20:59:14 +00:00
2019-08-15 04:30:37 +00:00
var stack = default_include ? microstack_with_include : microstack ;
2019-07-26 20:59:14 +00:00
var id = Guid . NewGuid ( ) . ToString ( ) ;
2019-08-28 03:22:57 +00:00
var matches = source_files . PMap ( e = > RunStack ( stack , new RawSourceFile ( e )
{
Path = Path . Combine ( Consts . BSACreationDir , id , e . Paths . Last ( ) )
} ) ) ;
2019-07-26 20:59:14 +00:00
foreach ( var match in matches )
{
2019-09-23 21:37:10 +00:00
if ( match is IgnoredDirectly ) Error ( $"File required for BSA {source.Path} creation doesn't exist: {match.To}" ) ;
2019-07-26 20:59:14 +00:00
ExtraFiles . Add ( match ) ;
2019-09-12 23:44:35 +00:00
}
;
2019-07-26 20:59:14 +00:00
CreateBSA directive ;
2019-07-30 03:32:52 +00:00
using ( var bsa = new BSAReader ( source . AbsolutePath ) )
2019-07-26 20:59:14 +00:00
{
2019-09-12 23:44:35 +00:00
directive = new CreateBSA
2019-07-26 20:59:14 +00:00
{
2019-07-28 23:04:23 +00:00
To = source . Path ,
2019-07-26 20:59:14 +00:00
TempID = id ,
2019-09-12 23:44:35 +00:00
Type = ( uint ) bsa . HeaderType ,
FileFlags = ( uint ) bsa . FileFlags ,
ArchiveFlags = ( uint ) bsa . ArchiveFlags
2019-07-26 20:59:14 +00:00
} ;
2019-09-12 23:44:35 +00:00
}
2019-07-26 20:59:14 +00:00
2019-09-12 23:44:35 +00:00
;
2019-07-26 20:59:14 +00:00
2019-09-12 23:44:35 +00:00
return directive ;
2019-07-26 20:59:14 +00:00
} ;
}
2019-08-15 04:30:37 +00:00
private Func < RawSourceFile , Directive > IncludeALL ( )
{
return source = >
{
var inline = source . EvolveTo < InlineFile > ( ) ;
inline . SourceData = File . ReadAllBytes ( source . AbsolutePath ) . ToBase64 ( ) ;
return inline ;
} ;
}
2019-07-23 04:27:26 +00:00
private Func < RawSourceFile , Directive > IgnoreDisabledMods ( )
{
2019-09-02 22:36:57 +00:00
var always_enabled = ModInis . Where ( f = > IsAlwaysEnabled ( f . Value ) ) . Select ( f = > f . Key ) . ToHashSet ( ) ;
2019-09-03 21:17:00 +00:00
var all_enabled_mods = SelectedProfiles
. SelectMany ( p = > File . ReadAllLines ( Path . Combine ( MO2Folder , "profiles" , p , "modlist.txt" ) ) )
. Where ( line = > line . StartsWith ( "+" ) | | line . EndsWith ( "_separator" ) )
. Select ( line = > line . Substring ( 1 ) )
. Concat ( always_enabled )
. Select ( line = > Path . Combine ( "mods" , line ) + "\\" )
. ToList ( ) ;
2019-09-12 23:44:35 +00:00
2019-07-23 04:27:26 +00:00
return source = >
{
2019-09-12 23:44:35 +00:00
if ( ! source . Path . StartsWith ( "mods" ) | | all_enabled_mods . Any ( mod = > source . Path . StartsWith ( mod ) ) )
return null ;
2019-09-03 21:17:00 +00:00
var r = source . EvolveTo < IgnoredDirectly > ( ) ;
r . Reason = "Disabled Mod" ;
return r ;
2019-07-23 04:27:26 +00:00
} ;
}
2019-09-02 22:36:57 +00:00
private static bool IsAlwaysEnabled ( dynamic data )
{
if ( data = = null )
return false ;
if ( data . General ! = null & & data . General . notes ! = null & &
data . General . notes . Contains ( Consts . WABBAJACK_ALWAYS_ENABLE ) )
return true ;
if ( data . General ! = null & & data . General . comments ! = null & &
data . General . notes . Contains ( Consts . WABBAJACK_ALWAYS_ENABLE ) )
return true ;
return false ;
}
2019-09-12 23:44:35 +00:00
/// <summary>
2019-09-14 04:35:42 +00:00
/// This matches files based purely on filename, and then creates a binary patch.
/// In practice this is fine, because a single file tends to only come from one archive.
2019-09-12 23:44:35 +00:00
/// </summary>
/// <returns></returns>
2019-07-21 22:47:17 +00:00
private Func < RawSourceFile , Directive > IncludePatches ( )
{
2019-08-20 04:57:08 +00:00
var indexed = IndexedFiles . Values
2019-09-12 23:44:35 +00:00
. SelectMany ( f = > f )
. GroupBy ( f = > Path . GetFileName ( f . Paths . Last ( ) ) . ToLower ( ) )
. ToDictionary ( f = > f . Key ) ;
2019-07-21 22:47:17 +00:00
return source = >
{
2019-09-12 23:44:35 +00:00
if ( ! indexed . TryGetValue ( Path . GetFileName ( source . File . Paths . Last ( ) . ToLower ( ) ) , out var value ) )
return null ;
var found = value . OrderByDescending ( f = > ( f . TopLevelArchive ? ? f ) . LastModified ) . First ( ) ;
var e = source . EvolveTo < PatchedFromArchive > ( ) ;
e . ArchiveHashPath = found . MakeRelativePaths ( ) ;
e . To = source . Path ;
2019-09-14 03:44:07 +00:00
e . Hash = source . File . Hash ;
2019-09-12 23:44:35 +00:00
Utils . TryGetPatch ( found . Hash , source . File . Hash , out e . Patch ) ;
return e ;
2019-07-21 22:47:17 +00:00
} ;
}
2019-07-21 12:42:29 +00:00
private Func < RawSourceFile , Directive > IncludeModIniData ( )
{
return source = >
{
if ( source . Path . StartsWith ( "mods\\" ) & & source . Path . EndsWith ( "\\meta.ini" ) )
{
var e = source . EvolveTo < InlineFile > ( ) ;
e . SourceData = File . ReadAllBytes ( source . AbsolutePath ) . ToBase64 ( ) ;
return e ;
}
2019-09-12 23:44:35 +00:00
2019-07-21 12:42:29 +00:00
return null ;
} ;
}
private Func < RawSourceFile , Directive > IgnoreGameFiles ( )
{
var start_dir = Consts . GameFolderFilesDir + "\\" ;
return source = >
{
if ( source . Path . StartsWith ( start_dir ) )
{
var i = source . EvolveTo < IgnoredDirectly > ( ) ;
i . Reason = "Default game file" ;
return i ;
}
2019-09-12 23:44:35 +00:00
2019-07-21 12:42:29 +00:00
return null ;
} ;
}
private Func < RawSourceFile , Directive > IncludeThisProfile ( )
{
2019-09-03 21:17:00 +00:00
var correct_profiles = SelectedProfiles . Select ( p = > Path . Combine ( "profiles" , p ) + "\\" ) . ToList ( ) ;
2019-07-21 12:42:29 +00:00
return source = >
{
2019-09-03 21:17:00 +00:00
if ( correct_profiles . Any ( p = > source . Path . StartsWith ( p ) ) )
2019-07-21 12:42:29 +00:00
{
byte [ ] data ;
if ( source . Path . EndsWith ( "\\modlist.txt" ) )
data = ReadAndCleanModlist ( source . AbsolutePath ) ;
else
data = File . ReadAllBytes ( source . AbsolutePath ) ;
var e = source . EvolveTo < InlineFile > ( ) ;
e . SourceData = data . ToBase64 ( ) ;
return e ;
}
2019-09-12 23:44:35 +00:00
2019-07-21 12:42:29 +00:00
return null ;
} ;
}
private byte [ ] ReadAndCleanModlist ( string absolutePath )
{
var lines = File . ReadAllLines ( absolutePath ) ;
lines = ( from line in lines
2019-09-12 23:44:35 +00:00
where ! ( line . StartsWith ( "-" ) & & ! line . EndsWith ( "_separator" ) )
select line ) . ToArray ( ) ;
return Encoding . UTF8 . GetBytes ( string . Join ( "\r\n" , lines ) ) ;
2019-07-21 12:42:29 +00:00
}
private Func < RawSourceFile , Directive > IgnoreOtherProfiles ( )
{
2019-09-03 21:17:00 +00:00
var profiles = SelectedProfiles
. Select ( p = > Path . Combine ( "profiles" , p ) + "\\" )
. ToList ( ) ;
2019-07-21 12:42:29 +00:00
return source = >
{
2019-09-03 21:17:00 +00:00
if ( source . Path . StartsWith ( "profiles\\" ) )
2019-07-21 12:42:29 +00:00
{
2019-09-12 23:44:35 +00:00
if ( profiles . Any ( profile = > source . Path . StartsWith ( profile ) ) ) return null ;
2019-07-21 12:42:29 +00:00
var c = source . EvolveTo < IgnoredDirectly > ( ) ;
2019-09-03 21:17:00 +00:00
c . Reason = "File not for selected profiles" ;
2019-07-21 12:42:29 +00:00
return c ;
}
2019-09-12 23:44:35 +00:00
2019-07-21 12:42:29 +00:00
return null ;
} ;
}
2019-07-21 04:40:54 +00:00
private Func < RawSourceFile , Directive > IgnoreEndsWith ( string v )
{
2019-09-12 23:44:35 +00:00
var reason = string . Format ( "Ignored because path ends with {0}" , v ) ;
2019-07-21 04:40:54 +00:00
return source = >
{
if ( source . Path . EndsWith ( v ) )
{
var result = source . EvolveTo < IgnoredDirectly > ( ) ;
result . Reason = reason ;
return result ;
}
2019-09-12 23:44:35 +00:00
2019-07-21 04:40:54 +00:00
return null ;
} ;
}
private Func < RawSourceFile , Directive > IgnoreRegex ( string p )
{
2019-09-12 23:44:35 +00:00
var reason = string . Format ( "Ignored because path matches regex {0}" , p ) ;
2019-07-21 04:40:54 +00:00
var regex = new Regex ( p ) ;
return source = >
{
if ( regex . IsMatch ( source . Path ) )
{
var result = source . EvolveTo < IgnoredDirectly > ( ) ;
result . Reason = reason ;
return result ;
}
2019-09-12 23:44:35 +00:00
2019-07-21 04:40:54 +00:00
return null ;
} ;
}
2019-08-22 22:05:16 +00:00
private Func < RawSourceFile , Directive > IncludeRegex ( string pattern )
{
var regex = new Regex ( pattern ) ;
return source = >
{
if ( regex . IsMatch ( source . Path ) )
{
var result = source . EvolveTo < InlineFile > ( ) ;
result . SourceData = File . ReadAllBytes ( source . AbsolutePath ) . ToBase64 ( ) ;
return result ;
}
2019-09-12 23:44:35 +00:00
2019-08-22 22:05:16 +00:00
return null ;
} ;
}
2019-07-21 04:40:54 +00:00
private Func < RawSourceFile , Directive > DropAll ( )
{
2019-09-12 23:44:35 +00:00
return source = >
{
2019-07-21 04:40:54 +00:00
var result = source . EvolveTo < NoMatch > ( ) ;
result . Reason = "No Match in Stack" ;
2019-08-02 22:31:13 +00:00
Info ( $"No match for: {source.Path}" ) ;
2019-07-21 04:40:54 +00:00
return result ;
2019-07-22 22:17:46 +00:00
} ;
2019-07-21 04:40:54 +00:00
}
private Func < RawSourceFile , Directive > DirectMatch ( )
{
return source = >
{
2019-08-20 04:57:08 +00:00
if ( IndexedFiles . TryGetValue ( source . Hash , out var found ) )
2019-07-21 04:40:54 +00:00
{
var result = source . EvolveTo < FromArchive > ( ) ;
2019-08-02 22:31:13 +00:00
2019-09-12 23:44:35 +00:00
var match = found . Where ( f = >
Path . GetFileName ( f . Paths [ f . Paths . Length - 1 ] ) = = Path . GetFileName ( source . Path ) )
. OrderBy ( f = > f . Paths . Length )
. FirstOrDefault ( ) ;
2019-08-02 22:31:13 +00:00
2019-07-21 04:40:54 +00:00
if ( match = = null )
2019-08-26 23:02:49 +00:00
match = found . OrderBy ( f = > f . Paths . Length ) . FirstOrDefault ( ) ;
2019-07-21 04:40:54 +00:00
2019-08-20 04:57:08 +00:00
result . ArchiveHashPath = match . MakeRelativePaths ( ) ;
2019-07-21 04:40:54 +00:00
return result ;
}
2019-09-12 23:44:35 +00:00
2019-07-21 04:40:54 +00:00
return null ;
} ;
}
private Func < RawSourceFile , Directive > IgnoreStartsWith ( string v )
{
2019-09-12 23:44:35 +00:00
var reason = string . Format ( "Ignored because path starts with {0}" , v ) ;
2019-07-21 04:40:54 +00:00
return source = >
{
if ( source . Path . StartsWith ( v ) )
{
var result = source . EvolveTo < IgnoredDirectly > ( ) ;
result . Reason = reason ;
return result ;
}
2019-09-12 23:44:35 +00:00
2019-07-21 04:40:54 +00:00
return null ;
} ;
}
2019-07-22 22:17:46 +00:00
internal void PatchExecutable ( )
{
2019-09-08 03:34:18 +00:00
Utils . Log ( "Exporting Installer" ) ;
2019-09-12 23:44:35 +00:00
var settings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling . Auto } ;
2019-07-22 22:17:46 +00:00
var executable = Assembly . GetExecutingAssembly ( ) . Location ;
var out_path = Path . Combine ( Path . GetDirectoryName ( executable ) , MO2Profile + ".exe" ) ;
Info ( "Patching Executable {0}" , Path . GetFileName ( out_path ) ) ;
File . Copy ( executable , out_path , true ) ;
using ( var os = File . OpenWrite ( out_path ) )
using ( var bw = new BinaryWriter ( os ) )
{
2019-09-12 23:44:35 +00:00
var orig_pos = os . Length ;
2019-07-22 22:17:46 +00:00
os . Position = os . Length ;
2019-09-12 23:44:35 +00:00
//using (var compressor = new BZip2OutputStream(bw.BaseStream))
/ * using ( var sw = new StreamWriter ( compressor ) )
using ( var writer = new JsonTextWriter ( sw ) )
{
var serializer = new JsonSerializer ( ) ;
serializer . TypeNameHandling = TypeNameHandling . Auto ;
serializer . Serialize ( writer , ModList ) ;
} * /
//bw.Write(data);
var formatter = new BinaryFormatter ( ) ;
2019-09-14 04:35:42 +00:00
using ( var compressed = LZ4Stream . Encode ( bw . BaseStream ,
new LZ4EncoderSettings { CompressionLevel = LZ4Level . L10_OPT } , true ) )
{
2019-09-12 23:44:35 +00:00
formatter . Serialize ( compressed , ModList ) ;
2019-09-14 04:35:42 +00:00
}
2019-09-12 23:44:35 +00:00
2019-07-22 22:17:46 +00:00
bw . Write ( orig_pos ) ;
2019-09-12 15:32:13 +00:00
bw . Write ( Encoding . ASCII . GetBytes ( Consts . ModListMagic ) ) ;
2019-07-22 22:17:46 +00:00
}
}
2019-09-12 23:44:35 +00:00
public class IndexedFileMatch
{
public IndexedArchive Archive ;
public IndexedArchiveEntry Entry ;
public DateTime LastModified ;
}
2019-07-21 04:40:54 +00:00
}
2019-09-12 23:44:35 +00:00
}