2021-09-27 12:42:46 +00:00
using System ;
using System.Collections.Generic ;
2024-08-25 21:21:16 +00:00
using System.Diagnostics ;
2021-09-27 12:42:46 +00:00
using System.Globalization ;
using System.IO ;
using System.Linq ;
using System.Text ;
2022-02-05 15:47:15 +00:00
using System.Text.Json ;
2022-01-27 07:05:51 +00:00
using System.Text.RegularExpressions ;
2021-09-27 12:42:46 +00:00
using System.Threading ;
using System.Threading.Tasks ;
using IniParser ;
using IniParser.Model.Configuration ;
using IniParser.Parser ;
2021-12-31 22:00:03 +00:00
using Microsoft.Extensions.DependencyInjection ;
2021-09-27 12:42:46 +00:00
using Microsoft.Extensions.Logging ;
using Wabbajack.Common ;
using Wabbajack.Compression.BSA ;
2022-02-05 15:47:15 +00:00
using Wabbajack.Compression.Zip ;
2021-09-27 12:42:46 +00:00
using Wabbajack.Downloaders ;
2021-10-13 03:59:54 +00:00
using Wabbajack.Downloaders.GameFile ;
2021-09-27 12:42:46 +00:00
using Wabbajack.DTOs ;
2022-10-23 22:07:55 +00:00
using Wabbajack.DTOs.BSA.FileStates ;
2021-09-27 12:42:46 +00:00
using Wabbajack.DTOs.Directives ;
using Wabbajack.DTOs.DownloadStates ;
2024-08-25 21:21:16 +00:00
using Wabbajack.DTOs.Interventions ;
2021-09-27 12:42:46 +00:00
using Wabbajack.DTOs.JsonConverters ;
2023-01-21 19:36:12 +00:00
using Wabbajack.Hashing.PHash ;
2022-10-23 22:07:55 +00:00
using Wabbajack.Hashing.xxHash64 ;
2021-09-27 12:42:46 +00:00
using Wabbajack.Installer.Utilities ;
using Wabbajack.Networking.WabbajackClientApi ;
using Wabbajack.Paths ;
using Wabbajack.Paths.IO ;
2022-05-17 05:26:59 +00:00
using Wabbajack.RateLimiter ;
2021-09-27 12:42:46 +00:00
using Wabbajack.VFS ;
2021-10-23 16:51:17 +00:00
namespace Wabbajack.Installer ;
public class StandardInstaller : AInstaller < StandardInstaller >
2021-09-27 12:42:46 +00:00
{
2021-10-23 16:51:17 +00:00
public StandardInstaller ( ILogger < StandardInstaller > logger ,
InstallerConfiguration config ,
IGameLocator gameLocator , FileExtractor . FileExtractor extractor ,
DTOSerializer jsonSerializer , Context vfs , FileHashCache fileHashCache ,
2023-01-21 19:36:12 +00:00
DownloadDispatcher downloadDispatcher , ParallelOptions parallelOptions , IResource < IInstaller > limiter , Client wjClient , IImageLoader imageLoader ) :
2021-10-23 16:51:17 +00:00
base ( logger , config , gameLocator , extractor , jsonSerializer , vfs , fileHashCache , downloadDispatcher ,
2023-01-21 19:36:12 +00:00
parallelOptions , limiter , wjClient , imageLoader )
2021-09-27 12:42:46 +00:00
{
2021-11-03 05:03:41 +00:00
MaxSteps = 14 ;
2021-10-23 16:51:17 +00:00
}
2021-09-27 12:42:46 +00:00
2021-12-31 22:00:03 +00:00
public static StandardInstaller Create ( IServiceProvider provider , InstallerConfiguration configuration )
{
return new StandardInstaller ( provider . GetRequiredService < ILogger < StandardInstaller > > ( ) ,
configuration ,
provider . GetRequiredService < IGameLocator > ( ) ,
provider . GetRequiredService < FileExtractor . FileExtractor > ( ) ,
provider . GetRequiredService < DTOSerializer > ( ) ,
provider . GetRequiredService < Context > ( ) ,
provider . GetRequiredService < FileHashCache > ( ) ,
provider . GetRequiredService < DownloadDispatcher > ( ) ,
provider . GetRequiredService < ParallelOptions > ( ) ,
2022-05-17 05:26:59 +00:00
provider . GetRequiredService < IResource < IInstaller > > ( ) ,
2023-01-21 19:36:12 +00:00
provider . GetRequiredService < Client > ( ) ,
provider . GetRequiredService < IImageLoader > ( ) ) ;
2021-12-31 22:00:03 +00:00
}
2021-10-23 16:51:17 +00:00
public override async Task < bool > Begin ( CancellationToken token )
{
if ( token . IsCancellationRequested ) return false ;
2022-09-20 23:18:32 +00:00
_logger . LogInformation ( "Installing: {Name} - {Version}" , _configuration . ModList . Name , _configuration . ModList . Version ) ;
2021-10-23 16:51:17 +00:00
await _wjClient . SendMetric ( MetricNames . BeginInstall , ModList . Name ) ;
2022-01-28 12:01:49 +00:00
NextStep ( Consts . StepPreparing , "Configuring Installer" , 0 ) ;
2021-10-23 16:51:17 +00:00
_logger . LogInformation ( "Configuring Processor" ) ;
if ( _configuration . GameFolder = = default )
_configuration . GameFolder = _gameLocator . GameLocation ( _configuration . Game ) ;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
if ( _configuration . GameFolder = = default )
2021-09-27 12:42:46 +00:00
{
2021-10-23 16:51:17 +00:00
var otherGame = _configuration . Game . MetaData ( ) . CommonlyConfusedWith
. Where ( g = > _gameLocator . IsInstalled ( g ) ) . Select ( g = > g . MetaData ( ) ) . FirstOrDefault ( ) ;
if ( otherGame ! = null )
_logger . LogError (
"In order to do a proper install Wabbajack needs to know where your {lookingFor} folder resides. However this game doesn't seem to be installed, we did however find an installed " +
"copy of {otherGame}, did you install the wrong game?" ,
_configuration . Game . MetaData ( ) . HumanFriendlyGameName , otherGame . HumanFriendlyGameName ) ;
else
_logger . LogError (
"In order to do a proper install Wabbajack needs to know where your {lookingFor} folder resides. However this game doesn't seem to be installed." ,
_configuration . Game . MetaData ( ) . HumanFriendlyGameName ) ;
return false ;
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
if ( ! _configuration . GameFolder . DirectoryExists ( ) )
{
_logger . LogError ( "Located game {game} at \"{gameFolder}\" but the folder does not exist!" ,
_configuration . Game , _configuration . GameFolder ) ;
return false ;
}
2021-09-27 12:42:46 +00:00
2021-12-27 23:15:30 +00:00
_logger . LogInformation ( "Install Folder: {InstallFolder}" , _configuration . Install ) ;
_logger . LogInformation ( "Downloads Folder: {DownloadFolder}" , _configuration . Downloads ) ;
_logger . LogInformation ( "Game Folder: {GameFolder}" , _configuration . GameFolder ) ;
_logger . LogInformation ( "Wabbajack Folder: {WabbajackFolder}" , KnownFolders . EntryPoint ) ;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
_configuration . Install . CreateDirectory ( ) ;
_configuration . Downloads . CreateDirectory ( ) ;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
await OptimizeModlist ( token ) ;
2023-05-07 20:32:18 +00:00
if ( token . IsCancellationRequested ) return false ;
2022-11-10 09:47:41 +00:00
2021-10-23 16:51:17 +00:00
await HashArchives ( token ) ;
2023-05-07 20:32:18 +00:00
if ( token . IsCancellationRequested ) return false ;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
await DownloadArchives ( token ) ;
2023-05-07 20:32:18 +00:00
if ( token . IsCancellationRequested ) return false ;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
await HashArchives ( token ) ;
2023-05-07 20:32:18 +00:00
if ( token . IsCancellationRequested ) return false ;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
var missing = ModList . Archives . Where ( a = > ! HashedArchives . ContainsKey ( a . Hash ) ) . ToList ( ) ;
if ( missing . Count > 0 )
{
2024-08-25 21:21:16 +00:00
if ( missing . Any ( m = > m . State is not Nexus ) )
{
ShowMissingManualReport ( missing . Where ( m = > m . State is not Nexus ) . ToArray ( ) ) ;
return false ;
}
2021-10-23 16:51:17 +00:00
foreach ( var a in missing )
_logger . LogCritical ( "Unable to download {name} ({primaryKeyString})" , a . Name ,
a . State . PrimaryKeyString ) ;
_logger . LogCritical ( "Cannot continue, was unable to download one or more archives" ) ;
return false ;
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
await ExtractModlist ( token ) ;
2023-05-07 20:32:18 +00:00
if ( token . IsCancellationRequested ) return false ;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
await PrimeVFS ( ) ;
2021-09-27 12:42:46 +00:00
2021-11-03 05:03:41 +00:00
await BuildFolderStructure ( ) ;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
await InstallArchives ( token ) ;
2023-05-07 20:32:18 +00:00
if ( token . IsCancellationRequested ) return false ;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
await InstallIncludedFiles ( token ) ;
2023-05-07 20:32:18 +00:00
if ( token . IsCancellationRequested ) return false ;
2021-09-27 12:42:46 +00:00
2022-11-10 10:35:51 +00:00
await WriteMetaFiles ( token ) ;
2023-05-07 20:32:18 +00:00
if ( token . IsCancellationRequested ) return false ;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
await BuildBSAs ( token ) ;
2023-05-07 20:32:18 +00:00
if ( token . IsCancellationRequested ) return false ;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
// TODO: Port this
await GenerateZEditMerges ( token ) ;
2023-05-07 20:32:18 +00:00
if ( token . IsCancellationRequested ) return false ;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
await ForcePortable ( ) ;
await RemapMO2File ( ) ;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
CreateOutputMods ( ) ;
2021-09-27 12:42:46 +00:00
2022-11-10 09:47:41 +00:00
SetScreenSizeInPrefs ( ) ;
2022-11-09 18:56:32 +00:00
2021-10-23 16:51:17 +00:00
await ExtractedModlistFolder ! . DisposeAsync ( ) ;
await _wjClient . SendMetric ( MetricNames . FinishInstall , ModList . Name ) ;
2021-09-27 12:42:46 +00:00
2022-01-28 12:01:49 +00:00
NextStep ( Consts . StepFinished , "Finished" , 1 ) ;
2021-10-23 16:51:17 +00:00
_logger . LogInformation ( "Finished Installation" ) ;
return true ;
}
2021-09-27 12:42:46 +00:00
2024-08-25 21:21:16 +00:00
private void ShowMissingManualReport ( Archive [ ] toArray )
{
_logger . LogError ( "Writing Manual helper report" ) ;
var report = _configuration . Downloads . Combine ( "MissingManuals.html" ) ;
{
using var writer = new StreamWriter ( report . Open ( FileMode . Create , FileAccess . Write , FileShare . None ) ) ;
writer . Write ( "<html><head><title>Missing Manual Downloads</title></head><body>" ) ;
writer . Write ( "<h1>Missing Manual Downloads</h1>" ) ;
writer . Write (
"<p>Wabbajack was unable to download the following archives automaticall. Please download them manually and place them in the downloads folder you chose during the install setup.</p>" ) ;
foreach ( var archive in toArray )
{
switch ( archive . State )
{
case Manual manual :
writer . Write ( $"<h3>{archive.Name}</h1>" ) ;
writer . Write ( $"<p>{manual.Prompt}</p>" ) ;
writer . Write ( $"<p>Download URL: <a href=\" { manual . Url } \ ">{manual.Url}</a></p>" ) ;
break ;
case MediaFire mediaFire :
writer . Write ( $"<h3>{archive.Name}</h1>" ) ;
writer . Write ( $"<p>Download URL: <a href=\" { mediaFire . Url } \ ">{mediaFire.Url}</a></p>" ) ;
break ;
default :
writer . Write ( $"<h3>{archive.Name}</h1>" ) ;
writer . Write ( $"<p>Unknown download type</p>" ) ;
writer . Write ( $"<p>Primary Key (may not be helpful): <a href=\" { archive . State . PrimaryKeyString } \ ">{archive.State.PrimaryKeyString}</a></p>" ) ;
break ;
}
}
writer . Write ( "</body></html>" ) ;
}
Process . Start ( new ProcessStartInfo ( "cmd.exe" , $"/c start {report}" )
{
CreateNoWindow = true ,
} ) ;
}
2022-10-07 22:14:01 +00:00
private Task RemapMO2File ( )
2021-10-23 16:51:17 +00:00
{
var iniFile = _configuration . Install . Combine ( "ModOrganizer.ini" ) ;
2022-10-07 22:14:01 +00:00
if ( ! iniFile . FileExists ( ) ) return Task . CompletedTask ;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
_logger . LogInformation ( "Remapping ModOrganizer.ini" ) ;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
var iniData = iniFile . LoadIniFile ( ) ;
var settings = iniData [ "Settings" ] ;
2022-08-20 14:26:54 +00:00
settings [ "download_directory" ] = _configuration . Downloads . ToString ( ) . Replace ( "\\" , "/" ) ;
2021-10-23 16:51:17 +00:00
iniData . SaveIniFile ( iniFile ) ;
2022-10-07 22:14:01 +00:00
return Task . CompletedTask ;
2021-10-23 16:51:17 +00:00
}
2021-10-21 03:18:15 +00:00
2021-10-23 16:51:17 +00:00
private void CreateOutputMods ( )
{
2022-09-26 23:39:50 +00:00
// Non MO2 Installs won't have this
var profileDir = _configuration . Install . Combine ( "profiles" ) ;
if ( ! profileDir . DirectoryExists ( ) ) return ;
2022-11-10 09:47:41 +00:00
2022-09-27 21:42:36 +00:00
profileDir
2021-10-23 16:51:17 +00:00
. EnumerateFiles ( )
. Where ( f = > f . FileName = = Consts . SettingsIni )
. Do ( f = >
{
if ( ! f . FileExists ( ) )
2021-09-27 12:42:46 +00:00
{
2021-10-23 16:51:17 +00:00
_logger . LogInformation ( "settings.ini is null for {profile}, skipping" , f ) ;
return ;
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
var ini = f . LoadIniFile ( ) ;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
var overwrites = ini [ "custom_overrides" ] ;
if ( overwrites = = null )
{
_logger . LogInformation ( "No custom overwrites found, skipping" ) ;
return ;
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
overwrites ! . Do ( keyData = >
{
var v = keyData . Value ;
var mod = _configuration . Install . Combine ( Consts . MO2ModFolderName , ( RelativePath ) v ) ;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
mod . CreateDirectory ( ) ;
2021-09-27 12:42:46 +00:00
} ) ;
2021-10-23 16:51:17 +00:00
} ) ;
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
private async Task ForcePortable ( )
{
var path = _configuration . Install . Combine ( "portable.txt" ) ;
if ( path . FileExists ( ) ) return ;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
try
{
await path . WriteAllTextAsync ( "Created by Wabbajack" ) ;
2021-09-27 12:42:46 +00:00
}
2021-10-23 16:51:17 +00:00
catch ( Exception e )
2021-09-27 12:42:46 +00:00
{
2021-10-23 16:51:17 +00:00
_logger . LogCritical ( e , "Could not create portable.txt in {_configuration.Install}" ,
_configuration . Install ) ;
}
}
2022-11-10 10:35:51 +00:00
private async Task WriteMetaFiles ( CancellationToken token )
2021-10-23 16:51:17 +00:00
{
2022-06-08 03:48:13 +00:00
_logger . LogInformation ( "Looking for downloads by size" ) ;
var bySize = UnoptimizedArchives . ToLookup ( x = > x . Size ) ;
_logger . LogInformation ( "Writing Metas" ) ;
await _configuration . Downloads . EnumerateFiles ( )
2022-11-14 15:22:55 +00:00
. Where ( download = > download . Extension ! = Ext . Meta )
2022-06-08 03:48:13 +00:00
. PDoAll ( async download = >
2021-10-23 16:51:17 +00:00
{
2022-11-10 10:35:51 +00:00
var metaFile = download . WithExtension ( Ext . Meta ) ;
2022-06-08 03:48:13 +00:00
var found = bySize [ download . Size ( ) ] ;
2024-07-20 09:58:04 +00:00
Hash hash = default ;
try
{
hash = await FileHashCache . FileHashCachedAsync ( download , token ) ;
}
catch ( Exception ex )
{
_logger . LogError ( $"Failed to get hash for file {download}!" ) ;
throw ;
}
2022-06-08 03:48:13 +00:00
var archive = found . FirstOrDefault ( f = > f . Hash = = hash ) ;
2022-11-10 10:35:51 +00:00
IEnumerable < string > meta ;
if ( archive = = default )
2021-09-27 12:42:46 +00:00
{
2022-11-10 10:35:51 +00:00
// archive is not part of the Modlist
if ( metaFile . FileExists ( ) )
2022-06-08 03:48:13 +00:00
{
2022-11-10 10:35:51 +00:00
try
{
var parsed = metaFile . LoadIniFile ( ) ;
2022-11-10 14:23:30 +00:00
if ( parsed [ "General" ] is not null & & (
parsed [ "General" ] [ "removed" ] is null | |
parsed [ "General" ] [ "removed" ] . Equals ( bool . FalseString , StringComparison . OrdinalIgnoreCase ) ) )
2022-11-10 10:35:51 +00:00
{
// add removed=true to files not part of the Modlist so they don't show up in MO2
parsed [ "General" ] [ "removed" ] = "true" ;
_logger . LogInformation ( "Writing {FileName}" , metaFile . FileName ) ;
parsed . SaveIniFile ( metaFile ) ;
}
}
catch ( Exception )
2022-06-08 03:48:13 +00:00
{
return ;
}
2022-11-10 10:35:51 +00:00
return ;
2022-06-08 03:48:13 +00:00
}
2022-11-10 10:35:51 +00:00
// create new meta file if missing
meta = new [ ]
2021-09-27 12:42:46 +00:00
{
2022-11-10 10:35:51 +00:00
"[General]" ,
"removed=true"
} ;
}
else
{
if ( metaFile . FileExists ( ) )
{
try
{
var parsed = metaFile . LoadIniFile ( ) ;
if ( parsed [ "General" ] is not null & & parsed [ "General" ] [ "unknownArchive" ] is null )
{
// meta doesn't have an associated archive
return ;
}
}
catch ( Exception )
{
// ignored
}
2021-09-27 12:42:46 +00:00
}
2022-11-10 10:35:51 +00:00
meta = AddInstalled ( _downloadDispatcher . MetaIni ( archive ) ) ;
2021-10-23 16:51:17 +00:00
}
2022-11-10 09:47:41 +00:00
2022-06-08 03:48:13 +00:00
_logger . LogInformation ( "Writing {FileName}" , metaFile . FileName ) ;
await metaFile . WriteAllLinesAsync ( meta , token ) ;
2021-10-23 16:51:17 +00:00
} ) ;
}
2022-11-10 10:35:51 +00:00
private static IEnumerable < string > AddInstalled ( IEnumerable < string > getMetaIni )
2021-10-23 16:51:17 +00:00
{
2022-05-28 22:53:52 +00:00
yield return "[General]" ;
yield return "installed=true" ;
2022-11-10 09:47:41 +00:00
2021-10-23 16:51:17 +00:00
foreach ( var f in getMetaIni )
{
yield return f ;
2021-09-27 12:42:46 +00:00
}
2021-10-23 16:51:17 +00:00
}
private async Task BuildBSAs ( CancellationToken token )
{
var bsas = ModList . Directives . OfType < CreateBSA > ( ) . ToList ( ) ;
2022-10-23 22:07:55 +00:00
_logger . LogInformation ( "Generating debug caches" ) ;
2022-10-24 04:56:57 +00:00
var indexedByDestination = UnoptimizedDirectives . ToDictionary ( d = > d . To ) ;
2021-10-23 16:51:17 +00:00
_logger . LogInformation ( "Building {bsasCount} bsa files" , bsas . Count ) ;
2022-10-01 05:21:58 +00:00
NextStep ( "Installing" , "Building BSAs" , bsas . Count ) ;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
foreach ( var bsa in bsas )
2021-09-27 12:42:46 +00:00
{
2022-10-01 05:21:58 +00:00
UpdateProgress ( 1 ) ;
2021-10-23 16:51:17 +00:00
_logger . LogInformation ( "Building {bsaTo}" , bsa . To . FileName ) ;
2022-10-24 23:28:03 +00:00
var sourceDir = _configuration . Install . Combine ( Consts . BSACreationDir , bsa . TempID ) ;
2021-10-23 16:51:17 +00:00
2022-06-08 03:48:13 +00:00
await using var a = BSADispatch . CreateBuilder ( bsa . State , _manager ) ;
2022-10-23 22:07:55 +00:00
var streams = await bsa . FileStates . PMapAllBatchedAsync ( _limiter , async state = >
2021-09-27 12:42:46 +00:00
{
2021-10-23 16:51:17 +00:00
var fs = sourceDir . Combine ( state . Path ) . Open ( FileMode . Open , FileAccess . Read , FileShare . Read ) ;
await a . AddFile ( state , fs , token ) ;
return fs ;
} ) . ToList ( ) ;
_logger . LogInformation ( "Writing {bsaTo}" , bsa . To ) ;
2022-05-17 22:32:53 +00:00
var outPath = _configuration . Install . Combine ( bsa . To ) ;
2022-11-10 09:47:41 +00:00
2022-10-24 04:56:57 +00:00
await using ( var outStream = outPath . Open ( FileMode . Create , FileAccess . Write , FileShare . None ) )
{
await a . Build ( outStream , token ) ;
}
2022-11-10 09:47:41 +00:00
2021-10-23 16:51:17 +00:00
streams . Do ( s = > s . Dispose ( ) ) ;
2022-11-10 09:47:41 +00:00
2022-10-01 05:21:58 +00:00
await FileHashCache . FileHashWriteCache ( outPath , bsa . Hash ) ;
2021-10-23 16:51:17 +00:00
sourceDir . DeleteDirectory ( ) ;
2022-11-10 09:47:41 +00:00
2022-10-23 22:07:55 +00:00
_logger . LogInformation ( "Verifying {bsaTo}" , bsa . To ) ;
var reader = await BSADispatch . Open ( outPath ) ;
var results = await reader . Files . PMapAllBatchedAsync ( _limiter , async state = >
{
var sf = await state . GetStreamFactory ( token ) ;
await using var stream = await sf . GetStream ( ) ;
var hash = await stream . Hash ( token ) ;
var astate = bsa . FileStates . First ( f = > f . Path = = state . Path ) ;
2022-10-24 23:28:03 +00:00
var srcDirective = indexedByDestination [ Consts . BSACreationDir . Combine ( bsa . TempID , astate . Path ) ] ;
2022-10-23 22:07:55 +00:00
//DX10Files are lossy
2022-11-10 09:47:41 +00:00
if ( astate is not BA2DX10File & & srcDirective . IsDeterministic )
2022-10-23 22:07:55 +00:00
ThrowOnNonMatchingHash ( bsa , srcDirective , astate , hash ) ;
return ( srcDirective , hash ) ;
} ) . ToHashSet ( ) ;
2021-09-27 12:42:46 +00:00
}
2022-10-24 23:28:03 +00:00
var bsaDir = _configuration . Install . Combine ( Consts . BSACreationDir ) ;
2021-10-23 16:51:17 +00:00
if ( bsaDir . DirectoryExists ( ) )
2021-09-27 12:42:46 +00:00
{
2022-10-24 23:28:03 +00:00
_logger . LogInformation ( "Removing temp folder {bsaCreationDir}" , Consts . BSACreationDir ) ;
2021-10-23 16:51:17 +00:00
bsaDir . DeleteDirectory ( ) ;
}
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
private async Task InstallIncludedFiles ( CancellationToken token )
{
_logger . LogInformation ( "Writing inline files" ) ;
2022-01-28 12:01:49 +00:00
NextStep ( Consts . StepInstalling , "Installing Included Files" , ModList . Directives . OfType < InlineFile > ( ) . Count ( ) ) ;
2021-10-23 16:51:17 +00:00
await ModList . Directives
. OfType < InlineFile > ( )
. PDoAll ( async directive = >
2021-09-27 12:42:46 +00:00
{
2021-11-02 13:40:59 +00:00
UpdateProgress ( 1 ) ;
2021-10-23 16:51:17 +00:00
var outPath = _configuration . Install . Combine ( directive . To ) ;
outPath . Delete ( ) ;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
switch ( directive )
2021-09-27 12:42:46 +00:00
{
2021-10-23 16:51:17 +00:00
case RemappedInlineFile file :
await WriteRemappedFile ( file ) ;
2022-10-01 05:21:58 +00:00
await FileHashCache . FileHashCachedAsync ( outPath , token ) ;
2021-10-23 16:51:17 +00:00
break ;
default :
2022-10-23 22:07:55 +00:00
var hash = await outPath . WriteAllHashedAsync ( await LoadBytesFromPath ( directive . SourceDataID ) , token ) ;
2022-10-25 02:12:14 +00:00
if ( ! Consts . KnownModifiedFiles . Contains ( directive . To . FileName ) )
2022-10-24 04:56:57 +00:00
ThrowOnNonMatchingHash ( directive , hash ) ;
2022-10-01 05:21:58 +00:00
await FileHashCache . FileHashWriteCache ( outPath , directive . Hash ) ;
2021-10-23 16:51:17 +00:00
break ;
}
} ) ;
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
private void SetScreenSizeInPrefs ( )
{
2022-09-26 23:39:50 +00:00
var profilesPath = _configuration . Install . Combine ( "profiles" ) ;
2022-11-10 09:47:41 +00:00
2022-10-01 05:31:20 +00:00
// Don't remap files for Native Game Compiler games
if ( ! profilesPath . DirectoryExists ( ) ) return ;
2021-10-23 16:51:17 +00:00
if ( _configuration . SystemParameters = = null )
_logger . LogWarning ( "No SystemParameters set, ignoring ini settings for system parameters" ) ;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
var config = new IniParserConfiguration { AllowDuplicateKeys = true , AllowDuplicateSections = true } ;
2022-01-27 07:05:51 +00:00
config . CommentRegex = new Regex ( @"^(#|;)(.*)" ) ;
2021-10-23 16:51:17 +00:00
var oblivionPath = ( RelativePath ) "Oblivion.ini" ;
2021-09-27 12:42:46 +00:00
2022-09-26 23:39:50 +00:00
if ( profilesPath . DirectoryExists ( ) )
{
foreach ( var file in profilesPath . EnumerateFiles ( )
. Where ( f = > ( ( string ) f . FileName ) . EndsWith ( "refs.ini" ) | | f . FileName = = oblivionPath ) )
try
{
var parser = new FileIniDataParser ( new IniDataParser ( config ) ) ;
var data = parser . ReadFile ( file . ToString ( ) ) ;
var 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" ] =
2022-10-07 22:14:01 +00:00
_configuration . SystemParameters ! . ScreenWidth . ToString ( CultureInfo . CurrentCulture ) ;
2022-09-26 23:39:50 +00:00
data . Sections [ "Display" ] [ "iSize H" ] =
_configuration . SystemParameters . ScreenHeight . ToString ( CultureInfo . CurrentCulture ) ;
modified = true ;
}
2021-09-27 12:42:46 +00:00
2022-09-26 23:39:50 +00:00
if ( data . Sections [ "MEMORY" ] ! = null )
if ( data . Sections [ "MEMORY" ] [ "VideoMemorySizeMb" ] ! = null )
{
data . Sections [ "MEMORY" ] [ "VideoMemorySizeMb" ] =
2022-10-07 22:14:01 +00:00
_configuration . SystemParameters ! . EnbLEVRAMSize . ToString ( CultureInfo . CurrentCulture ) ;
2022-09-26 23:39:50 +00:00
modified = true ;
}
if ( ! modified ) continue ;
parser . WriteFile ( file . ToString ( ) , data ) ;
_logger . LogTrace ( "Remapped screen size in {file}" , file ) ;
}
catch ( Exception ex )
{
_logger . LogCritical ( ex , "Skipping screen size remap for {file} due to parse error." , file ) ;
}
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
var tweaksPath = ( RelativePath ) "SSEDisplayTweaks.ini" ;
foreach ( var file in _configuration . Install . EnumerateFiles ( )
. Where ( f = > f . FileName = = tweaksPath ) )
try
{
var parser = new FileIniDataParser ( new IniDataParser ( config ) ) ;
var data = parser . ReadFile ( file . ToString ( ) ) ;
var modified = false ;
if ( data . Sections [ "Render" ] ! = null )
if ( data . Sections [ "Render" ] [ "Resolution" ] ! = null )
{
data . Sections [ "Render" ] [ "Resolution" ] =
2022-10-07 22:14:01 +00:00
$"{_configuration.SystemParameters!.ScreenWidth.ToString(CultureInfo.CurrentCulture)}x{_configuration.SystemParameters.ScreenHeight.ToString(CultureInfo.CurrentCulture)}" ;
2021-10-23 16:51:17 +00:00
modified = true ;
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
if ( modified )
parser . WriteFile ( file . ToString ( ) , data ) ;
}
catch ( Exception ex )
{
_logger . LogCritical ( ex , "Skipping screen size remap for {file} due to parse error." , file ) ;
}
2022-11-10 09:47:41 +00:00
2022-06-25 05:31:02 +00:00
// The Witcher 3
if ( _configuration . Game = = Game . Witcher3 )
{
var name = ( RelativePath ) "user.settings" ;
foreach ( var file in _configuration . Install . Combine ( "profiles" ) . EnumerateFiles ( )
. Where ( f = > f . FileName = = name ) )
{
try
{
var parser = new FileIniDataParser ( new IniDataParser ( config ) ) ;
var data = parser . ReadFile ( file . ToString ( ) ) ;
data [ "Viewport" ] [ "Resolution" ] =
$"{_configuration.SystemParameters!.ScreenWidth}x{_configuration.SystemParameters!.ScreenHeight}" ;
parser . WriteFile ( file . ToString ( ) , data ) ;
}
catch ( Exception ex )
{
_logger . LogInformation ( ex , "While remapping user.settings" ) ;
}
}
}
2021-10-23 16:51:17 +00:00
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
private async Task WriteRemappedFile ( RemappedInlineFile directive )
{
var data = Encoding . UTF8 . GetString ( await LoadBytesFromPath ( directive . SourceDataID ) ) ;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
var gameFolder = _configuration . GameFolder . ToString ( ) ;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
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 ( "\\" , "/" ) ) ;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
data = data . Replace ( Consts . MO2_PATH_MAGIC_BACK , _configuration . Install . ToString ( ) ) ;
data = data . Replace ( Consts . MO2_PATH_MAGIC_DOUBLE_BACK ,
_configuration . Install . ToString ( ) . Replace ( "\\" , "\\\\" ) ) ;
data = data . Replace ( Consts . MO2_PATH_MAGIC_FORWARD , _configuration . Install . ToString ( ) . Replace ( "\\" , "/" ) ) ;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
data = data . Replace ( Consts . DOWNLOAD_PATH_MAGIC_BACK , _configuration . Downloads . ToString ( ) ) ;
data = data . Replace ( Consts . DOWNLOAD_PATH_MAGIC_DOUBLE_BACK ,
_configuration . Downloads . ToString ( ) . Replace ( "\\" , "\\\\" ) ) ;
data = data . Replace ( Consts . DOWNLOAD_PATH_MAGIC_FORWARD ,
_configuration . Downloads . ToString ( ) . Replace ( "\\" , "/" ) ) ;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
await _configuration . Install . Combine ( directive . To ) . WriteAllTextAsync ( data ) ;
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
public async Task GenerateZEditMerges ( CancellationToken token )
{
2022-10-01 05:21:58 +00:00
var patches = _configuration . ModList
2021-10-23 16:51:17 +00:00
. Directives
. OfType < MergedPatch > ( )
2022-10-01 05:21:58 +00:00
. ToList ( ) ;
NextStep ( "Installing" , "Generating ZEdit Merges" , patches . Count ) ;
2021-09-27 12:42:46 +00:00
2022-10-07 22:14:01 +00:00
await patches . PMapAllBatchedAsync ( _limiter , async m = >
2022-10-01 05:21:58 +00:00
{
UpdateProgress ( 1 ) ;
_logger . LogInformation ( "Generating zEdit merge: {to}" , m . To ) ;
2021-09-27 12:42:46 +00:00
2022-10-01 05:21:58 +00:00
var srcData = ( await m . Sources . SelectAsync ( async s = >
await _configuration . Install . Combine ( s . RelativePath ) . ReadAllBytesAsync ( token ) )
. ToReadOnlyCollection ( ) )
. ConcatArrays ( ) ;
2021-10-23 16:51:17 +00:00
2022-10-01 05:21:58 +00:00
var patchData = await LoadBytesFromPath ( m . PatchID ) ;
await using var fs = _configuration . Install . Combine ( m . To )
. Open ( FileMode . Create , FileAccess . ReadWrite , FileShare . None ) ;
2022-10-18 03:41:28 +00:00
try
{
2022-10-23 21:28:44 +00:00
var hash = await BinaryPatching . ApplyPatch ( new MemoryStream ( srcData ) , new MemoryStream ( patchData ) , fs ) ;
ThrowOnNonMatchingHash ( m , hash ) ;
2022-10-18 03:41:28 +00:00
}
catch ( Exception ex )
{
_logger . LogError ( ex , "While creating zEdit merge, entering debugging mode" ) ;
foreach ( var source in m . Sources )
{
var hash = await _configuration . Install . Combine ( source . RelativePath ) . Hash ( ) ;
_logger . LogInformation ( "For {Source} expected hash {Expected} got {Got}" , source . RelativePath , source . Hash , hash ) ;
}
throw ;
}
2022-10-01 05:21:58 +00:00
return m ;
} ) . ToList ( ) ;
2021-09-27 12:42:46 +00:00
}
2022-02-05 15:47:15 +00:00
public static async Task < ModList > Load ( DTOSerializer dtos , DownloadDispatcher dispatcher , ModlistMetadata metadata , CancellationToken token )
{
var archive = new Archive
{
State = dispatcher . Parse ( new Uri ( metadata . Links . Download ) ) ! ,
Size = metadata . DownloadMetadata ! . Size ,
Hash = metadata . DownloadMetadata . Hash
} ;
2023-10-09 20:25:10 +00:00
await using var stream = await dispatcher . ChunkedSeekableStream ( archive , token ) ;
2022-02-05 15:47:15 +00:00
await using var reader = new ZipReader ( stream ) ;
var entry = ( await reader . GetFiles ( ) ) . First ( e = > e . FileName = = "modlist" ) ;
2023-10-09 20:25:10 +00:00
using var ms = new MemoryStream ( ) ;
2022-02-05 15:47:15 +00:00
await reader . Extract ( entry , ms , token ) ;
ms . Position = 0 ;
return JsonSerializer . Deserialize < ModList > ( ms , dtos . Options ) ! ;
}
2022-11-10 09:47:41 +00:00
}