2021-09-27 12:42:46 +00:00
using System ;
using System.Collections.Generic ;
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 ;
using Wabbajack.DTOs.Directives ;
using Wabbajack.DTOs.DownloadStates ;
using Wabbajack.DTOs.JsonConverters ;
using Wabbajack.Installer.Utilities ;
using Wabbajack.Networking.WabbajackClientApi ;
using Wabbajack.Paths ;
using Wabbajack.Paths.IO ;
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 static RelativePath BSACreationDir = "TEMP_BSA_FILES" . ToRelativePath ( ) ;
public StandardInstaller ( ILogger < StandardInstaller > logger ,
InstallerConfiguration config ,
IGameLocator gameLocator , FileExtractor . FileExtractor extractor ,
DTOSerializer jsonSerializer , Context vfs , FileHashCache fileHashCache ,
DownloadDispatcher downloadDispatcher , ParallelOptions parallelOptions , Client wjClient ) :
base ( logger , config , gameLocator , extractor , jsonSerializer , vfs , fileHashCache , downloadDispatcher ,
parallelOptions , wjClient )
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 > ( ) ,
provider . GetRequiredService < Client > ( ) ) ;
}
2021-10-23 16:51:17 +00:00
public override async Task < bool > Begin ( CancellationToken token )
{
if ( token . IsCancellationRequested ) return false ;
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 ) ;
2021-11-02 13:40:59 +00:00
2021-10-23 16:51:17 +00:00
await HashArchives ( token ) ;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
await DownloadArchives ( token ) ;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
await HashArchives ( token ) ;
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 )
{
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 ) ;
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 ) ;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
await InstallIncludedFiles ( token ) ;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
await InstallIncludedDownloadMetas ( token ) ;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
await BuildBSAs ( token ) ;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
// TODO: Port this
await GenerateZEditMerges ( token ) ;
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
2021-10-23 16:51:17 +00:00
SetScreenSizeInPrefs ( ) ;
2021-09-27 12:42:46 +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
2021-10-23 16:51:17 +00:00
private async Task RemapMO2File ( )
{
var iniFile = _configuration . Install . Combine ( "ModOrganizer.ini" ) ;
if ( ! iniFile . FileExists ( ) ) return ;
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" ] ;
settings [ "download_directory" ] = _configuration . Downloads . ToString ( ) ;
iniData . SaveIniFile ( iniFile ) ;
}
2021-10-21 03:18:15 +00:00
2021-10-23 16:51:17 +00:00
private void CreateOutputMods ( )
{
_configuration . Install . Combine ( "profiles" )
. 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 ) ;
}
}
private async Task InstallIncludedDownloadMetas ( CancellationToken token )
{
await ModList . Archives
. PDoAll ( async archive = >
{
if ( HashedArchives . TryGetValue ( archive . Hash , out var paths ) )
2021-09-27 12:42:46 +00:00
{
2021-10-23 16:51:17 +00:00
var metaPath = paths . WithExtension ( Ext . Meta ) ;
if ( ! metaPath . FileExists ( ) & & archive . State is not GameFileSource )
2021-09-27 12:42:46 +00:00
{
2021-10-23 16:51:17 +00:00
var meta = AddInstalled ( _downloadDispatcher . MetaIni ( archive ) ) ;
await metaPath . WriteAllLinesAsync ( meta , token ) ;
2021-09-27 12:42:46 +00:00
}
2021-10-23 16:51:17 +00:00
}
} ) ;
}
private IEnumerable < string > AddInstalled ( IEnumerable < string > getMetaIni )
{
foreach ( var f in getMetaIni )
{
yield return f ;
if ( f = = "[General]" ) yield return "installed=true" ;
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 ( ) ;
_logger . LogInformation ( "Building {bsasCount} bsa files" , 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
{
2021-10-23 16:51:17 +00:00
_logger . LogInformation ( "Building {bsaTo}" , bsa . To . FileName ) ;
var sourceDir = _configuration . Install . Combine ( BSACreationDir , bsa . TempID ) ;
var a = BSADispatch . CreateBuilder ( bsa . State , _manager ) ;
var streams = await bsa . FileStates . PMapAll ( 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 ) ;
await using var outStream = _configuration . Install . Combine ( bsa . To )
. Open ( FileMode . Create , FileAccess . Write , FileShare . None ) ;
await a . Build ( outStream , token ) ;
streams . Do ( s = > s . Dispose ( ) ) ;
sourceDir . DeleteDirectory ( ) ;
2021-09-27 12:42:46 +00:00
}
2021-10-23 16:51:17 +00:00
var bsaDir = _configuration . Install . Combine ( BSACreationDir ) ;
if ( bsaDir . DirectoryExists ( ) )
2021-09-27 12:42:46 +00:00
{
2021-10-23 16:51:17 +00:00
_logger . LogInformation ( "Removing temp folder {bsaCreationDir}" , BSACreationDir ) ;
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 ) ;
break ;
default :
await outPath . WriteAllBytesAsync ( await LoadBytesFromPath ( directive . SourceDataID ) , token ) ;
break ;
}
} ) ;
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
private void SetScreenSizeInPrefs ( )
{
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" ;
foreach ( var file in _configuration . Install . Combine ( "profiles" ) . EnumerateFiles ( )
. Where ( f = > ( ( string ) f . FileName ) . EndsWith ( "refs.ini" ) | | f . FileName = = oblivionPath ) )
try
2021-09-27 12:42:46 +00:00
{
2021-10-23 16:51:17 +00:00
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" ] =
_configuration . SystemParameters . ScreenWidth . ToString ( CultureInfo . CurrentCulture ) ;
data . Sections [ "Display" ] [ "iSize H" ] =
_configuration . SystemParameters . ScreenHeight . ToString ( CultureInfo . CurrentCulture ) ;
modified = true ;
}
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
if ( data . Sections [ "MEMORY" ] ! = null )
if ( data . Sections [ "MEMORY" ] [ "VideoMemorySizeMb" ] ! = null )
2021-09-27 12:42:46 +00:00
{
2021-10-23 16:51:17 +00:00
data . Sections [ "MEMORY" ] [ "VideoMemorySizeMb" ] =
_configuration . SystemParameters . EnbLEVRAMSize . ToString ( CultureInfo . CurrentCulture ) ;
modified = true ;
2021-09-27 12:42:46 +00:00
}
2022-01-27 07:05:51 +00:00
if ( ! modified ) continue ;
parser . WriteFile ( file . ToString ( ) , data ) ;
_logger . LogTrace ( "Remapped screen size in {file}" , file ) ;
2021-10-23 16:51:17 +00:00
}
catch ( Exception ex )
2021-09-27 12:42:46 +00:00
{
2021-10-23 16:51:17 +00:00
_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" ] =
$"{_configuration.SystemParameters.ScreenWidth.ToString(CultureInfo.CurrentCulture)}x{_configuration.SystemParameters.ScreenHeight.ToString(CultureInfo.CurrentCulture)}" ;
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 ) ;
}
}
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 )
{
await _configuration . ModList
. Directives
. OfType < MergedPatch > ( )
. PDoAll ( async m = >
{
_logger . LogInformation ( "Generating zEdit merge: {to}" , m . To ) ;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
var srcData = ( await m . Sources . SelectAsync ( async s = >
2021-09-27 12:42:46 +00:00
await _configuration . Install . Combine ( s . RelativePath ) . ReadAllBytesAsync ( token ) )
. ToReadOnlyCollection ( ) )
2021-10-23 16:51:17 +00:00
. ConcatArrays ( ) ;
2021-09-27 12:42:46 +00:00
2021-10-23 16:51:17 +00:00
var patchData = await LoadBytesFromPath ( m . PatchID ) ;
await using var fs = _configuration . Install . Combine ( m . To )
. Open ( FileMode . Create , FileAccess . Write , FileShare . None ) ;
await BinaryPatching . ApplyPatch ( new MemoryStream ( srcData ) , new MemoryStream ( patchData ) , fs ) ;
} ) ;
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
} ;
var stream = await dispatcher . ChunkedSeekableStream ( archive , token ) ;
await using var reader = new ZipReader ( stream ) ;
var entry = ( await reader . GetFiles ( ) ) . First ( e = > e . FileName = = "modlist" ) ;
var ms = new MemoryStream ( ) ;
await reader . Extract ( entry , ms , token ) ;
ms . Position = 0 ;
return JsonSerializer . Deserialize < ModList > ( ms , dtos . Options ) ! ;
}
2021-09-27 12:42:46 +00:00
}