2021-05-20 09:54:57 +00:00
using System ;
2020-03-09 21:07:57 +00:00
using System.ComponentModel.DataAnnotations ;
2020-01-04 10:25:53 +00:00
using System.Globalization ;
2019-07-23 04:17:52 +00:00
using System.IO ;
using System.Linq ;
using System.Text ;
2019-12-04 01:26:26 +00:00
using System.Threading.Tasks ;
2019-12-03 21:56:18 +00:00
using System.Threading ;
2019-08-26 23:02:49 +00:00
using System.Windows ;
2019-12-06 04:58:18 +00:00
using Alphaleonis.Win32.Filesystem ;
using IniParser ;
2020-01-03 15:01:17 +00:00
using IniParser.Model ;
2019-12-16 06:33:44 +00:00
using IniParser.Model.Configuration ;
2019-12-06 04:58:18 +00:00
using IniParser.Parser ;
2019-07-23 04:17:52 +00:00
using Wabbajack.Common ;
2019-12-04 04:12:08 +00:00
using Wabbajack.Lib.CompilationSteps.CompilationErrors ;
2019-10-16 03:10:34 +00:00
using Wabbajack.Lib.Downloaders ;
using Wabbajack.Lib.NexusApi ;
using Wabbajack.Lib.Validation ;
2019-09-18 21:33:23 +00:00
using Directory = Alphaleonis . Win32 . Filesystem . Directory ;
using File = Alphaleonis . Win32 . Filesystem . File ;
using Path = Alphaleonis . Win32 . Filesystem . Path ;
2020-01-18 14:48:55 +00:00
using SectionData = Wabbajack . Common . SectionData ;
2020-08-07 13:22:48 +00:00
using System.Collections.Generic ;
2021-05-20 09:54:57 +00:00
using Wabbajack.Common.IO ;
2021-03-19 22:37:58 +00:00
using Wabbajack.Lib.ModListRegistry ;
2020-09-12 20:23:03 +00:00
using Wabbajack.VirtualFileSystem ;
2019-07-23 04:17:52 +00:00
2019-10-16 03:10:34 +00:00
namespace Wabbajack.Lib
2019-07-23 04:17:52 +00:00
{
2019-11-18 00:17:06 +00:00
public class MO2Installer : AInstaller
2019-07-23 04:17:52 +00:00
{
2019-11-18 05:21:24 +00:00
public bool WarnOnOverwrite { get ; set ; } = true ;
2019-12-01 20:22:33 +00:00
public override ModManager ModManager = > ModManager . MO2 ;
2019-07-23 04:17:52 +00:00
2020-03-25 22:30:43 +00:00
public AbsolutePath ? GameFolder { get ; set ; }
2021-03-19 22:37:58 +00:00
public ModlistMetadata ? Metadata { get ; set ; }
2019-07-23 04:17:52 +00:00
2020-03-26 21:15:44 +00:00
public MO2Installer ( AbsolutePath archive , ModList modList , AbsolutePath outputFolder , AbsolutePath downloadFolder , SystemParameters parameters )
2019-12-01 20:22:33 +00:00
: base (
archive : archive ,
modList : modList ,
outputFolder : outputFolder ,
2020-01-07 13:50:11 +00:00
downloadFolder : downloadFolder ,
2020-04-04 18:26:14 +00:00
parameters : parameters ,
2020-09-08 22:15:33 +00:00
steps : 22 ,
2020-05-02 23:05:05 +00:00
game : modList . GameType )
2019-12-01 20:22:33 +00:00
{
2020-05-02 23:05:05 +00:00
var gameExe = Consts . GameFolderFilesDir . Combine ( modList . GameType . MetaData ( ) . MainExecutable ! ) ;
RedirectGamePath = modList . Directives . Any ( d = > d . To = = gameExe ) ;
2019-12-01 20:22:33 +00:00
}
2020-05-02 23:05:05 +00:00
public bool RedirectGamePath { get ; }
2019-12-04 01:26:26 +00:00
protected override async Task < bool > _Begin ( CancellationToken cancel )
2019-07-23 04:17:52 +00:00
{
2019-12-03 21:56:18 +00:00
if ( cancel . IsCancellationRequested ) return false ;
2020-06-14 13:13:29 +00:00
await Metrics . Send ( Metrics . BeginInstall , ModList . Name ) ;
2020-03-29 03:29:27 +00:00
Utils . Log ( "Configuring Processor" ) ;
2019-12-15 04:33:48 +00:00
2020-09-12 20:23:03 +00:00
FileExtractor2 . FavorPerfOverRAM = FavorPerfOverRam ;
2019-11-02 23:20:41 +00:00
2019-12-21 20:09:21 +00:00
if ( GameFolder = = null )
2020-04-12 19:35:17 +00:00
GameFolder = Game . TryGetGameLocation ( ) ;
2019-11-02 23:20:41 +00:00
2021-07-16 12:10:46 +00:00
if ( GameFolder is { Exists : false } )
{
Utils . Error ( $"Located game {Game.HumanFriendlyGameName} at \" { GameFolder . Value } \ " but the folder does not exist!" ) ;
return false ;
}
2019-11-02 23:20:41 +00:00
if ( GameFolder = = null )
{
2020-04-09 17:17:24 +00:00
var otherGame = Game . CommonlyConfusedWith . Where ( g = > g . MetaData ( ) . IsInstalled ) . Select ( g = > g . MetaData ( ) ) . FirstOrDefault ( ) ;
2020-03-19 02:10:55 +00:00
if ( otherGame ! = null )
{
2021-05-20 11:40:05 +00:00
Utils . Error ( new CriticalFailureIntervention (
$"In order to do a proper install Wabbajack needs to know where your {Game.HumanFriendlyGameName} folder resides. However this game doesn't seem to be installed, we did however find an installed " +
$"copy of {otherGame.HumanFriendlyGameName}, did you install the wrong game?" ,
$"Could not locate {Game.HumanFriendlyGameName}" ) ) ;
2020-03-19 02:10:55 +00:00
}
else
{
2021-05-20 11:40:05 +00:00
Utils . Error ( new CriticalFailureIntervention (
$"In order to do a proper install Wabbajack needs to know where your {Game.HumanFriendlyGameName} folder resides. However this game doesn't seem to be installed." ,
$"Could not locate {Game.HumanFriendlyGameName}" ) ) ;
2020-03-19 02:10:55 +00:00
}
2021-05-20 11:40:05 +00:00
Utils . Error ( "Exiting because we couldn't find the game folder." ) ;
2019-11-17 23:48:32 +00:00
return false ;
2019-11-02 23:20:41 +00:00
}
2020-10-01 12:24:26 +00:00
Utils . Log ( $"Install Folder: {OutputFolder}" ) ;
Utils . Log ( $"Downloads Folder: {DownloadFolder}" ) ;
Utils . Log ( $"Game Folder: {GameFolder.Value}" ) ;
Utils . Log ( $"Wabbajack Folder: {AbsolutePath.EntryPoint}" ) ;
2020-07-27 21:33:45 +00:00
var watcher = new DiskSpaceWatcher ( cancel , new [ ] { OutputFolder , DownloadFolder , GameFolder . Value , AbsolutePath . EntryPoint } , ( long ) 2 < < 31 ,
drive = >
{
Utils . Log ( $"Aborting due to low space on {drive.Name}" ) ;
Abort ( ) ;
} ) ;
var watcherTask = watcher . Start ( ) ;
2019-11-02 23:20:41 +00:00
2019-12-03 21:56:18 +00:00
if ( cancel . IsCancellationRequested ) return false ;
2019-11-24 23:03:36 +00:00
UpdateTracker . NextStep ( "Validating Game ESMs" ) ;
2020-05-25 17:34:25 +00:00
await ValidateGameESMs ( ) ;
2019-11-24 23:03:36 +00:00
2019-12-03 21:56:18 +00:00
if ( cancel . IsCancellationRequested ) return false ;
2021-02-06 16:43:11 +00:00
UpdateTracker . NextStep ( "Creating Output Folders" ) ;
2019-09-29 22:21:18 +00:00
2020-03-25 22:30:43 +00:00
OutputFolder . CreateDirectory ( ) ;
DownloadFolder . CreateDirectory ( ) ;
2019-07-23 04:17:52 +00:00
2020-03-25 22:30:43 +00:00
if ( OutputFolder . Combine ( Consts . MO2ModFolderName ) . IsDirectory & & WarnOnOverwrite )
2019-09-04 21:19:37 +00:00
{
2019-12-07 02:54:27 +00:00
if ( ( await Utils . Log ( new ConfirmUpdateOfExistingInstall { ModListName = ModList . Name , OutputFolder = OutputFolder } ) . Task ) = = ConfirmUpdateOfExistingInstall . Choice . Abort )
2019-09-18 21:33:23 +00:00
{
2019-12-21 19:37:48 +00:00
Utils . Log ( "Exiting installation at the request of the user, existing mods folder found." ) ;
2019-11-17 23:48:32 +00:00
return false ;
2019-09-18 21:33:23 +00:00
}
2019-09-04 21:19:37 +00:00
}
2019-09-14 04:35:42 +00:00
2021-02-04 10:05:33 +00:00
// Reduce to one thread if downloads on HDD, else use specified. Hashing on HDD has no benefit with more threads.
if ( new PhysicalDisk ( DownloadFolder . DriveInfo ( ) . Name ) . MediaType = = PhysicalDisk . MediaTypes . HDD & & ReduceHDDThreads ) DesiredThreads . OnNext ( 1 ) ; else DesiredThreads . OnNext ( DiskThreads ) ;
2019-12-03 21:56:18 +00:00
if ( cancel . IsCancellationRequested ) return false ;
2020-01-13 21:11:07 +00:00
UpdateTracker . NextStep ( "Optimizing ModList" ) ;
2019-12-04 01:26:26 +00:00
await OptimizeModlist ( ) ;
2019-08-24 23:20:54 +00:00
2019-12-03 21:56:18 +00:00
if ( cancel . IsCancellationRequested ) return false ;
2019-11-24 23:03:36 +00:00
UpdateTracker . NextStep ( "Hashing Archives" ) ;
2019-12-04 01:26:26 +00:00
await HashArchives ( ) ;
2019-11-24 23:03:36 +00:00
2021-02-01 06:41:51 +00:00
// Set to download thread count.
DesiredThreads . OnNext ( DownloadThreads ) ;
2019-12-03 21:56:18 +00:00
if ( cancel . IsCancellationRequested ) return false ;
2019-11-24 23:03:36 +00:00
UpdateTracker . NextStep ( "Downloading Missing Archives" ) ;
2019-12-04 01:26:26 +00:00
await DownloadArchives ( ) ;
2019-11-24 23:03:36 +00:00
2021-02-01 06:41:51 +00:00
// Reduce to one thread if downloads on HDD, else use specified. Hashing on HDD has no benefit with more threads.
if ( new PhysicalDisk ( DownloadFolder . DriveInfo ( ) . Name ) . MediaType = = PhysicalDisk . MediaTypes . HDD & & ReduceHDDThreads ) DesiredThreads . OnNext ( 1 ) ; else DesiredThreads . OnNext ( DiskThreads ) ;
2019-12-03 21:56:18 +00:00
if ( cancel . IsCancellationRequested ) return false ;
2019-11-24 23:03:36 +00:00
UpdateTracker . NextStep ( "Hashing Remaining Archives" ) ;
2019-12-04 01:26:26 +00:00
await HashArchives ( ) ;
2019-07-23 04:17:52 +00:00
var missing = ModList . Archives . Where ( a = > ! HashedArchives . ContainsKey ( a . Hash ) ) . ToList ( ) ;
if ( missing . Count > 0 )
{
foreach ( var a in missing )
2020-07-07 00:46:39 +00:00
Info ( $"Unable to download {a.Name} ({a.State.PrimaryKeyString})" ) ;
2019-08-07 23:06:38 +00:00
if ( IgnoreMissingFiles )
Info ( "Missing some archives, but continuing anyways at the request of the user" ) ;
else
Error ( "Cannot continue, was unable to download one or more archives" ) ;
2019-07-23 04:17:52 +00:00
}
2019-08-20 04:57:08 +00:00
2021-02-01 06:41:51 +00:00
// Reduce to two threads if output on HDD, else use specified. Installing files seems to have a slight benefit with two threads.
if ( new PhysicalDisk ( OutputFolder . DriveInfo ( ) . Name ) . MediaType = = PhysicalDisk . MediaTypes . HDD & & ReduceHDDThreads ) DesiredThreads . OnNext ( 2 ) ; else DesiredThreads . OnNext ( DiskThreads ) ;
2020-05-13 04:12:55 +00:00
if ( cancel . IsCancellationRequested ) return false ;
UpdateTracker . NextStep ( "Extracting Modlist contents" ) ;
await ExtractModlist ( ) ;
2019-12-03 21:56:18 +00:00
if ( cancel . IsCancellationRequested ) return false ;
2019-11-24 23:03:36 +00:00
UpdateTracker . NextStep ( "Priming VFS" ) ;
2019-12-07 02:54:27 +00:00
await PrimeVFS ( ) ;
2019-08-20 04:57:08 +00:00
2019-12-03 21:56:18 +00:00
if ( cancel . IsCancellationRequested ) return false ;
2019-11-24 23:03:36 +00:00
UpdateTracker . NextStep ( "Building Folder Structure" ) ;
2019-07-23 04:17:52 +00:00
BuildFolderStructure ( ) ;
2019-11-24 23:03:36 +00:00
2019-12-03 21:56:18 +00:00
if ( cancel . IsCancellationRequested ) return false ;
2019-11-24 23:03:36 +00:00
UpdateTracker . NextStep ( "Installing Archives" ) ;
2019-12-04 01:26:26 +00:00
await InstallArchives ( ) ;
2019-11-24 23:03:36 +00:00
2019-12-03 21:56:18 +00:00
if ( cancel . IsCancellationRequested ) return false ;
2019-11-24 23:03:36 +00:00
UpdateTracker . NextStep ( "Installing Included files" ) ;
2019-12-04 01:26:26 +00:00
await InstallIncludedFiles ( ) ;
2019-11-24 23:03:36 +00:00
2019-12-03 21:56:18 +00:00
if ( cancel . IsCancellationRequested ) return false ;
2019-11-24 23:03:36 +00:00
UpdateTracker . NextStep ( "Installing Archive Metas" ) ;
2019-12-04 01:26:26 +00:00
await InstallIncludedDownloadMetas ( ) ;
2019-11-24 23:03:36 +00:00
2019-12-03 21:56:18 +00:00
if ( cancel . IsCancellationRequested ) return false ;
2019-11-24 23:03:36 +00:00
UpdateTracker . NextStep ( "Building BSAs" ) ;
2019-12-04 01:26:26 +00:00
await BuildBSAs ( ) ;
2019-07-23 04:17:52 +00:00
2019-12-03 21:56:18 +00:00
if ( cancel . IsCancellationRequested ) return false ;
2019-11-24 23:03:36 +00:00
UpdateTracker . NextStep ( "Generating Merges" ) ;
2019-12-04 01:26:26 +00:00
await zEditIntegration . GenerateMerges ( this ) ;
2019-11-02 15:38:03 +00:00
2020-01-06 15:20:18 +00:00
UpdateTracker . NextStep ( "Set MO2 into portable" ) ;
2020-03-25 22:30:43 +00:00
await ForcePortable ( ) ;
2020-01-06 15:20:18 +00:00
2020-01-18 14:48:55 +00:00
UpdateTracker . NextStep ( "Create Empty Output Mods" ) ;
CreateOutputMods ( ) ;
2021-03-19 22:37:58 +00:00
UpdateTracker . NextStep ( "Updating System-specific ini settings and writing metadata" ) ;
2019-12-06 04:58:18 +00:00
SetScreenSizeInPrefs ( ) ;
2021-03-19 22:37:58 +00:00
await InstalledModLists . AddModListInstall ( Metadata , ModList , OutputFolder , DownloadFolder , ModListArchive ) ;
2020-09-08 22:15:33 +00:00
UpdateTracker . NextStep ( "Compacting files" ) ;
await CompactFiles ( ) ;
2019-12-06 04:58:18 +00:00
2019-11-24 23:03:36 +00:00
UpdateTracker . NextStep ( "Installation complete! You may exit the program." ) ;
2020-09-05 14:01:32 +00:00
await ExtractedModlistFolder ! . DisposeAsync ( ) ;
2020-06-14 13:13:29 +00:00
await Metrics . Send ( Metrics . FinishInstall , ModList . Name ) ;
2019-12-15 04:33:48 +00:00
2019-11-17 23:48:32 +00:00
return true ;
2019-08-26 23:02:49 +00:00
}
2020-09-08 22:15:33 +00:00
private async Task CompactFiles ( )
{
if ( this . UseCompression )
{
await OutputFolder . CompactFolder ( Queue , FileCompaction . Algorithm . XPRESS16K ) ;
}
}
2020-01-18 14:48:55 +00:00
private void CreateOutputMods ( )
{
2020-03-25 22:30:43 +00:00
OutputFolder . Combine ( "profiles" )
. EnumerateFiles ( true )
. Where ( f = > f . FileName = = Consts . SettingsIni )
. Do ( f = >
2020-01-18 14:48:55 +00:00
{
var ini = f . LoadIniFile ( ) ;
if ( ini = = null )
{
Utils . Log ( $"settings.ini is null for {f}, skipping" ) ;
return ;
}
var overwrites = ini . custom_overwrites ;
if ( overwrites = = null )
{
Utils . Log ( "No custom overwrites found, skipping" ) ;
return ;
}
if ( overwrites is SectionData data )
{
data . Coll . Do ( keyData = >
{
var v = keyData . Value ;
2020-03-25 22:30:43 +00:00
var mod = OutputFolder . Combine ( Consts . MO2ModFolderName , ( RelativePath ) v ) ;
2020-01-18 14:48:55 +00:00
2020-03-25 22:30:43 +00:00
mod . CreateDirectory ( ) ;
2020-01-18 14:48:55 +00:00
} ) ;
}
} ) ;
}
2020-03-25 22:30:43 +00:00
private async Task ForcePortable ( )
2020-01-06 15:20:18 +00:00
{
2020-03-25 22:30:43 +00:00
var path = OutputFolder . Combine ( "portable.txt" ) ;
if ( path . Exists ) return ;
2020-01-06 15:20:18 +00:00
try
{
2020-03-25 22:30:43 +00:00
await path . WriteAllTextAsync ( "Created by Wabbajack" ) ;
2020-01-06 15:20:18 +00:00
}
catch ( Exception e )
{
Utils . Error ( e , $"Could not create portable.txt in {OutputFolder}" ) ;
}
}
2019-11-18 05:21:24 +00:00
2019-12-04 01:26:26 +00:00
private async Task InstallIncludedDownloadMetas ( )
2019-11-04 04:36:25 +00:00
{
2020-09-11 01:22:07 +00:00
await ModList . Archives
2020-11-03 14:45:08 +00:00
. PMap ( Queue , UpdateTracker , async archive = >
2019-11-04 04:36:25 +00:00
{
2020-09-11 01:22:07 +00:00
if ( HashedArchives . TryGetValue ( archive . Hash , out var paths ) )
2020-09-09 22:50:43 +00:00
{
2020-09-11 01:22:07 +00:00
var metaPath = paths . WithExtension ( Consts . MetaFileExtension ) ;
2021-01-29 04:13:36 +00:00
if ( ! metaPath . Exists & & ! ( archive . State is GameFileSourceDownloader . State ) )
2020-09-09 22:50:43 +00:00
{
2020-09-11 01:22:07 +00:00
Status ( $"Writing {metaPath.FileName}" ) ;
var meta = AddInstalled ( archive . State . GetMetaIni ( ) ) . ToArray ( ) ;
await metaPath . WriteAllLinesAsync ( meta ) ;
2020-09-09 22:50:43 +00:00
}
2020-09-11 01:22:07 +00:00
}
2019-11-04 04:36:25 +00:00
} ) ;
}
2020-09-09 22:50:43 +00:00
private IEnumerable < string > AddInstalled ( string [ ] getMetaIni )
{
foreach ( var f in getMetaIni )
{
yield return f ;
if ( f = = "[General]" )
{
yield return "installed=true" ;
}
}
}
2020-05-25 17:34:25 +00:00
private async ValueTask ValidateGameESMs ( )
2019-11-02 21:08:37 +00:00
{
foreach ( var esm in ModList . Directives . OfType < CleanedESM > ( ) . ToList ( ) )
{
2020-03-25 22:30:43 +00:00
var filename = esm . To . FileName ;
2020-04-09 17:17:24 +00:00
var gameFile = GameFolder ! . Value . Combine ( ( RelativePath ) "Data" , filename ) ;
2019-11-02 21:08:37 +00:00
Utils . Log ( $"Validating {filename}" ) ;
2020-05-25 17:34:25 +00:00
var hash = await gameFile . FileHashAsync ( ) ;
2019-11-02 21:08:37 +00:00
if ( hash ! = esm . SourceESMHash )
{
2021-01-09 19:04:11 +00:00
Utils . ErrorThrow ( new InvalidGameESMError ( esm , hash ? ? Hash . Empty , gameFile ) ) ;
2019-11-02 21:08:37 +00:00
}
}
}
2019-12-04 01:26:26 +00:00
private async Task BuildBSAs ( )
2019-07-28 23:04:23 +00:00
{
var bsas = ModList . Directives . OfType < CreateBSA > ( ) . ToList ( ) ;
2019-07-30 03:32:52 +00:00
Info ( $"Building {bsas.Count} bsa files" ) ;
2019-07-28 23:04:23 +00:00
2019-12-04 01:26:26 +00:00
foreach ( var bsa in bsas )
2019-07-28 23:04:23 +00:00
{
Status ( $"Building {bsa.To}" ) ;
2020-07-19 23:09:59 +00:00
Info ( $"Building {bsa.To}" ) ;
2020-03-25 22:30:43 +00:00
var sourceDir = OutputFolder . Combine ( Consts . BSACreationDir , bsa . TempID ) ;
2019-07-28 23:04:23 +00:00
2020-03-25 22:30:43 +00:00
var bsaSize = bsa . FileStates . Select ( state = > sourceDir . Combine ( state . Path ) . Size ) . Sum ( ) ;
2020-03-09 21:07:57 +00:00
2020-07-20 01:19:56 +00:00
await using var a = await bsa . State . MakeBuilder ( bsaSize ) ;
2020-11-03 14:45:08 +00:00
var streams = await bsa . FileStates . PMap ( Queue , UpdateTracker , async state = >
2019-10-11 23:31:36 +00:00
{
2020-04-20 21:36:33 +00:00
Status ( $"Adding {state.Path} to BSA" ) ;
2020-05-25 17:34:25 +00:00
var fs = await sourceDir . Combine ( state . Path ) . OpenRead ( ) ;
2020-04-20 21:36:33 +00:00
await a . AddFile ( state , fs ) ;
return fs ;
} ) ;
Info ( $"Writing {bsa.To}" ) ;
await a . Build ( OutputFolder . Combine ( bsa . To ) ) ;
streams . Do ( s = > s . Dispose ( ) ) ;
2020-08-05 22:01:45 +00:00
2020-10-05 22:12:21 +00:00
await sourceDir . DeleteDirectory ( ) ;
2020-08-05 22:01:45 +00:00
if ( UseCompression )
await OutputFolder . Combine ( bsa . To ) . Compact ( FileCompaction . Algorithm . XPRESS16K ) ;
2019-12-04 01:26:26 +00:00
}
2019-09-18 21:33:23 +00:00
2020-03-25 22:30:43 +00:00
var bsaDir = OutputFolder . Combine ( Consts . BSACreationDir ) ;
if ( bsaDir . Exists )
2019-08-20 22:37:55 +00:00
{
Info ( $"Removing temp folder {Consts.BSACreationDir}" ) ;
2020-03-28 04:33:26 +00:00
await Utils . DeleteDirectory ( bsaDir ) ;
2019-08-20 22:37:55 +00:00
}
2019-07-28 23:04:23 +00:00
}
2019-12-04 01:26:26 +00:00
private async Task InstallIncludedFiles ( )
2019-07-23 04:27:26 +00:00
{
Info ( "Writing inline files" ) ;
2019-12-04 01:26:26 +00:00
await ModList . Directives
2019-09-14 04:35:42 +00:00
. OfType < InlineFile > ( )
2020-11-03 14:45:08 +00:00
. PMap ( Queue , UpdateTracker , async directive = >
2019-09-14 04:35:42 +00:00
{
2019-09-26 22:32:15 +00:00
Status ( $"Writing included file {directive.To}" ) ;
2020-03-25 22:30:43 +00:00
var outPath = OutputFolder . Combine ( directive . To ) ;
2020-05-26 11:31:11 +00:00
await outPath . DeleteAsync ( ) ;
2020-03-25 22:30:43 +00:00
switch ( directive )
{
case RemappedInlineFile file :
await WriteRemappedFile ( file ) ;
break ;
case CleanedESM esm :
await GenerateCleanedESM ( esm ) ;
break ;
default :
await outPath . WriteAllBytesAsync ( await LoadBytesFromPath ( directive . SourceDataID ) ) ;
break ;
}
2020-08-05 22:01:45 +00:00
if ( UseCompression )
await outPath . Compact ( FileCompaction . Algorithm . XPRESS16K ) ;
2019-09-14 04:35:42 +00:00
} ) ;
2019-07-23 04:27:26 +00:00
}
2020-03-25 22:30:43 +00:00
private async Task GenerateCleanedESM ( CleanedESM directive )
2019-08-25 03:46:32 +00:00
{
2020-03-25 22:30:43 +00:00
var filename = directive . To . FileName ;
2020-04-09 17:17:24 +00:00
var gameFile = GameFolder ! . Value . Combine ( ( RelativePath ) "Data" , filename ) ;
2019-08-25 03:46:32 +00:00
Info ( $"Generating cleaned ESM for {filename}" ) ;
2020-03-25 22:30:43 +00:00
if ( ! gameFile . Exists ) throw new InvalidDataException ( $"Missing {filename} at {gameFile}" ) ;
2019-08-25 03:46:32 +00:00
Status ( $"Hashing game version of {filename}" ) ;
2020-05-12 21:31:53 +00:00
var sha = await gameFile . FileHashCachedAsync ( ) ;
2019-08-25 03:46:32 +00:00
if ( sha ! = directive . SourceESMHash )
2019-09-14 04:35:42 +00:00
throw new InvalidDataException (
2019-12-21 20:09:21 +00:00
$"Cannot patch {filename} from the game folder because the hashes do not match. Have you already cleaned the file?" ) ;
2019-08-25 03:46:32 +00:00
2020-03-25 22:30:43 +00:00
var patchData = await LoadBytesFromPath ( directive . SourceDataID ) ;
var toFile = OutputFolder . Combine ( directive . To ) ;
2019-08-25 03:46:32 +00:00
Status ( $"Patching {filename}" ) ;
2020-05-25 17:34:25 +00:00
await using var output = await toFile . Create ( ) ;
await using var input = await gameFile . OpenRead ( ) ;
2020-03-25 22:30:43 +00:00
Utils . ApplyPatch ( input , ( ) = > new MemoryStream ( patchData ) , output ) ;
2019-08-25 03:46:32 +00:00
}
2019-12-06 04:58:18 +00:00
private void SetScreenSizeInPrefs ( )
{
2020-04-10 01:29:53 +00:00
if ( SystemParameters = = null )
{
throw new ArgumentNullException ( "System Parameters was null. Cannot set screen size prefs" ) ;
}
2019-12-17 23:17:44 +00:00
var config = new IniParserConfiguration { AllowDuplicateKeys = true , AllowDuplicateSections = true } ;
2021-03-24 22:20:59 +00:00
var oblivionPath = ( RelativePath ) "Oblivion.ini" ;
2020-03-25 22:49:32 +00:00
foreach ( var file in OutputFolder . Combine ( "profiles" ) . EnumerateFiles ( )
2021-03-24 22:20:59 +00:00
. Where ( f = > ( ( string ) f . FileName ) . EndsWith ( "refs.ini" ) | | f . FileName = = oblivionPath ) )
2019-12-06 04:58:18 +00:00
{
2020-01-03 15:01:17 +00:00
try
{
2020-01-04 02:52:17 +00:00
var parser = new FileIniDataParser ( new IniDataParser ( config ) ) ;
2020-03-25 22:49:32 +00:00
var data = parser . ReadFile ( ( string ) file ) ;
2020-03-10 04:11:11 +00:00
bool modified = false ;
if ( data . Sections [ "Display" ] ! = null )
{
if ( data . Sections [ "Display" ] [ "iSize W" ] ! = null & & data . Sections [ "Display" ] [ "iSize H" ] ! = null )
{
data . Sections [ "Display" ] [ "iSize W" ] =
SystemParameters . ScreenWidth . ToString ( CultureInfo . CurrentCulture ) ;
data . Sections [ "Display" ] [ "iSize H" ] =
SystemParameters . ScreenHeight . ToString ( CultureInfo . CurrentCulture ) ;
modified = true ;
}
2020-01-04 10:25:53 +00:00
2020-03-10 04:11:11 +00:00
}
if ( data . Sections [ "MEMORY" ] ! = null )
2020-01-04 02:52:17 +00:00
{
2020-03-10 04:11:11 +00:00
if ( data . Sections [ "MEMORY" ] [ "VideoMemorySizeMb" ] ! = null )
{
data . Sections [ "MEMORY" ] [ "VideoMemorySizeMb" ] =
SystemParameters . EnbLEVRAMSize . ToString ( CultureInfo . CurrentCulture ) ;
modified = true ;
}
2020-01-04 02:52:17 +00:00
}
2020-03-10 04:11:11 +00:00
if ( modified )
2020-03-25 22:49:32 +00:00
parser . WriteFile ( ( string ) file , data ) ;
2020-01-03 15:01:17 +00:00
}
2020-03-28 13:33:39 +00:00
catch ( Exception )
2020-01-03 15:01:17 +00:00
{
Utils . Log ( $"Skipping screen size remap for {file} due to parse error." ) ;
}
2019-12-06 04:58:18 +00:00
}
2021-03-24 22:20:59 +00:00
var tweaksPath = ( RelativePath ) "SSEDisplayTweaks.ini" ;
foreach ( var file in OutputFolder . EnumerateFiles ( )
. Where ( f = > f . FileName = = tweaksPath ) )
{
try
{
var parser = new FileIniDataParser ( new IniDataParser ( config ) ) ;
var data = parser . ReadFile ( ( string ) file ) ;
bool modified = false ;
if ( data . Sections [ "Render" ] ! = null )
{
if ( data . Sections [ "Render" ] [ "Resolution" ] ! = null )
{
data . Sections [ "Render" ] [ "Resolution" ] =
$"{SystemParameters.ScreenWidth.ToString(CultureInfo.CurrentCulture)}x{SystemParameters.ScreenHeight.ToString(CultureInfo.CurrentCulture)}" ;
modified = true ;
}
}
if ( modified )
parser . WriteFile ( ( string ) file , data ) ;
}
catch ( Exception )
{
Utils . Log ( $"Skipping screen size remap for {file} due to parse error." ) ;
}
}
2019-12-06 04:58:18 +00:00
}
2020-03-25 22:30:43 +00:00
private async Task WriteRemappedFile ( RemappedInlineFile directive )
2019-08-24 23:20:54 +00:00
{
2020-03-25 22:30:43 +00:00
var data = Encoding . UTF8 . GetString ( await LoadBytesFromPath ( directive . SourceDataID ) ) ;
2019-08-24 23:20:54 +00:00
2020-05-02 23:05:05 +00:00
var gameFolder = ( string ) ( RedirectGamePath ? Consts . GameFolderFilesDir . RelativeTo ( OutputFolder ) : GameFolder ! ) ;
data = data . Replace ( Consts . GAME_PATH_MAGIC_BACK , gameFolder ) ;
data = data . Replace ( Consts . GAME_PATH_MAGIC_DOUBLE_BACK , gameFolder . Replace ( "\\" , "\\\\" ) ) ;
data = data . Replace ( Consts . GAME_PATH_MAGIC_FORWARD , gameFolder . Replace ( "\\" , "/" ) ) ;
2019-08-24 23:20:54 +00:00
2020-03-25 22:30:43 +00:00
data = data . Replace ( Consts . MO2_PATH_MAGIC_BACK , ( string ) OutputFolder ) ;
data = data . Replace ( Consts . MO2_PATH_MAGIC_DOUBLE_BACK , ( ( string ) OutputFolder ) . Replace ( "\\" , "\\\\" ) ) ;
data = data . Replace ( Consts . MO2_PATH_MAGIC_FORWARD , ( ( string ) OutputFolder ) . Replace ( "\\" , "/" ) ) ;
2019-08-24 23:20:54 +00:00
2020-03-25 22:30:43 +00:00
data = data . Replace ( Consts . DOWNLOAD_PATH_MAGIC_BACK , ( string ) DownloadFolder ) ;
data = data . Replace ( Consts . DOWNLOAD_PATH_MAGIC_DOUBLE_BACK , ( ( string ) DownloadFolder ) . Replace ( "\\" , "\\\\" ) ) ;
data = data . Replace ( Consts . DOWNLOAD_PATH_MAGIC_FORWARD , ( ( string ) DownloadFolder ) . Replace ( "\\" , "/" ) ) ;
2019-09-03 22:12:39 +00:00
2020-03-25 22:30:43 +00:00
await OutputFolder . Combine ( directive . To ) . WriteAllTextAsync ( data ) ;
2019-07-23 04:17:52 +00:00
}
2019-12-20 22:06:20 +00:00
2021-05-20 09:54:57 +00:00
public static IErrorResponse CheckValidInstallPath ( AbsolutePath path , AbsolutePath ? downloadFolder , GameMetaData ? game )
2019-12-20 22:06:20 +00:00
{
2021-05-20 09:54:57 +00:00
// Check if null path
if ( string . IsNullOrEmpty ( path . ToString ( ) ) ) return ErrorResponse . Fail ( "Please select an install directory." ) ;
// Check if child of game folder
if ( game ? . TryGetGameLocation ( ) ! = null & & path . IsChildOf ( game . TryGetGameLocation ( ) ) ) return ErrorResponse . Fail ( "Cannot install to game directory." ) ;
// Check if child of Program Files
2021-06-12 09:36:51 +00:00
var programFilesPath = KnownFolders . ProgramFiles . Path ;
if ( programFilesPath ! = null )
{
if ( path . IsChildOf ( new AbsolutePath ( programFilesPath ) ) ) return ErrorResponse . Fail ( "Cannot install to Program Files directory." ) ;
}
2021-05-20 09:54:57 +00:00
// If the folder doesn't exist, it's empty so we don't need to check further
2020-03-27 03:33:24 +00:00
if ( ! path . Exists ) return ErrorResponse . Success ;
2019-12-20 22:31:35 +00:00
2020-01-13 21:11:07 +00:00
// Check folder does not have a Wabbajack ModList
2020-03-27 03:33:24 +00:00
if ( path . EnumerateFiles ( false ) . Where ( file = > file . Exists ) . Any ( file = > file . Extension = = Consts . ModListExtension ) )
2019-12-20 22:06:20 +00:00
{
2021-05-20 09:54:57 +00:00
return ErrorResponse . Fail ( $"Cannot install into a folder with a Wabbajack ModList inside of it." ) ;
2019-12-20 22:06:20 +00:00
}
2019-12-20 22:31:35 +00:00
2020-08-07 13:22:48 +00:00
// Check if folder is empty
2020-03-27 03:33:24 +00:00
if ( path . IsEmptyDirectory )
2019-12-20 22:31:35 +00:00
{
2020-03-27 03:33:24 +00:00
return ErrorResponse . Success ;
}
2019-12-22 01:30:01 +00:00
2020-08-07 13:22:48 +00:00
// Check if folders indicative of a previous install exist
var checks = new List < RelativePath > ( ) {
Consts . MO2ModFolderName ,
Consts . MO2ProfilesFolderName
} ;
if ( checks . All ( c = > path . Combine ( c ) . Exists ) )
{
return ErrorResponse . Success ;
}
2020-03-27 03:33:24 +00:00
// If we have a MO2 install, assume good to go
if ( path . EnumerateFiles ( false ) . Any ( file = >
{
if ( file . FileName = = Consts . ModOrganizer2Exe ) return true ;
if ( file . FileName = = Consts . ModOrganizer2Ini ) return true ;
return false ;
} ) )
{
return ErrorResponse . Success ;
}
// If we don't have a MO2 install, and there's any file that's not in the downloads folder, mark failure
if ( downloadFolder . HasValue & & path . EnumerateFiles ( true ) . All ( file = > file . InFolder ( downloadFolder . Value ) ) )
{
return ErrorResponse . Success ;
2019-12-20 22:31:35 +00:00
}
2020-07-25 23:38:00 +00:00
return ErrorResponse . Fail ( $"Either delete everything except the downloads folder, or pick a new location. Cannot install to this folder as it has unexpected files." ) ;
2019-12-20 22:06:20 +00:00
}
2019-07-23 04:17:52 +00:00
}
2019-09-24 15:26:44 +00:00
}