Complete refactoring of most code, indiscriminate application of .editorconfig and general cleanup.

This commit is contained in:
Ottermandias 2021-06-19 11:53:54 +02:00
parent 5332119a63
commit a19ec226c5
84 changed files with 3168 additions and 1709 deletions

32
Penumbra/Mod/Mod.cs Normal file
View file

@ -0,0 +1,32 @@
using System.Collections.Generic;
using System.IO;
using Penumbra.Util;
namespace Penumbra.Mod
{
public class Mod
{
public ModSettings Settings { get; }
public ModData Data { get; }
public ModCache Cache { get; }
public Mod( ModSettings settings, ModData data )
{
Settings = settings;
Data = data;
Cache = new ModCache();
}
public bool FixSettings()
=> Settings.FixInvalidSettings( Data.Meta );
public HashSet< GamePath > GetFiles( FileInfo file )
{
var relPath = new RelPath( file, Data.BasePath );
return ModFunctions.GetFilesForConfig( relPath, Settings, Data.Meta );
}
public override string ToString()
=> Data.Meta.Name;
}
}

57
Penumbra/Mod/ModCache.cs Normal file
View file

@ -0,0 +1,57 @@
using System.Collections.Generic;
using System.Linq;
using Penumbra.Meta;
using Penumbra.Util;
namespace Penumbra.Mod
{
public class ModCache
{
public Dictionary< Mod, (List< GamePath > Files, List< MetaManipulation > Manipulations) > Conflicts { get; private set; } = new();
public void AddConflict( Mod precedingMod, GamePath gamePath )
{
if( Conflicts.TryGetValue( precedingMod, out var conflicts ) && !conflicts.Files.Contains( gamePath ) )
{
conflicts.Files.Add( gamePath );
}
else
{
Conflicts[ precedingMod ] = ( new List< GamePath > { gamePath }, new List< MetaManipulation >() );
}
}
public void AddConflict( Mod precedingMod, MetaManipulation manipulation )
{
if( Conflicts.TryGetValue( precedingMod, out var conflicts ) && !conflicts.Manipulations.Contains( manipulation ) )
{
conflicts.Manipulations.Add( manipulation );
}
else
{
Conflicts[ precedingMod ] = ( new List< GamePath >(), new List< MetaManipulation > { manipulation } );
}
}
public void ClearConflicts()
=> Conflicts.Clear();
public void ClearFileConflicts()
{
Conflicts = Conflicts.Where( kvp => kvp.Value.Manipulations.Count > 0 ).ToDictionary( kvp => kvp.Key, kvp =>
{
kvp.Value.Files.Clear();
return kvp.Value;
} );
}
public void ClearMetaConflicts()
{
Conflicts = Conflicts.Where( kvp => kvp.Value.Files.Count > 0 ).ToDictionary( kvp => kvp.Key, kvp =>
{
kvp.Value.Manipulations.Clear();
return kvp.Value;
} );
}
}
}

380
Penumbra/Mod/ModCleanup.cs Normal file
View file

@ -0,0 +1,380 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using Dalamud.Plugin;
using Penumbra.Structs;
using Penumbra.Util;
namespace Penumbra.Mod
{
public class ModCleanup
{
private const string Duplicates = "Duplicates";
private const string Required = "Required";
private readonly DirectoryInfo _baseDir;
private readonly ModMeta _mod;
private SHA256? _hasher;
private readonly Dictionary< long, List< FileInfo > > _filesBySize = new();
private SHA256 Sha()
{
_hasher ??= SHA256.Create();
return _hasher;
}
private ModCleanup( DirectoryInfo baseDir, ModMeta mod )
{
_baseDir = baseDir;
_mod = mod;
BuildDict();
}
private void BuildDict()
{
foreach( var file in _baseDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) )
{
var fileLength = file.Length;
if( _filesBySize.TryGetValue( fileLength, out var files ) )
{
files.Add( file );
}
else
{
_filesBySize[ fileLength ] = new List< FileInfo >() { file };
}
}
}
private static Option FindOrCreateDuplicates( ModMeta meta )
{
static Option RequiredOption()
=> new()
{
OptionName = Required,
OptionDesc = "",
OptionFiles = new Dictionary< RelPath, HashSet< GamePath > >(),
};
if( meta.Groups.TryGetValue( Duplicates, out var duplicates ) )
{
var idx = duplicates.Options.FindIndex( o => o.OptionName == Required );
if( idx >= 0 )
{
return duplicates.Options[ idx ];
}
duplicates.Options.Add( RequiredOption() );
return duplicates.Options.Last();
}
meta.Groups.Add( Duplicates, new OptionGroup
{
GroupName = Duplicates,
SelectionType = SelectType.Single,
Options = new List< Option > { RequiredOption() },
} );
return meta.Groups[ Duplicates ].Options.First();
}
public static void Deduplicate( DirectoryInfo baseDir, ModMeta mod )
{
var dedup = new ModCleanup( baseDir, mod );
foreach( var pair in dedup._filesBySize.Where( pair => pair.Value.Count >= 2 ) )
{
if( pair.Value.Count == 2 )
{
if( CompareFilesDirectly( pair.Value[ 0 ], pair.Value[ 1 ] ) )
{
dedup.ReplaceFile( pair.Value[ 0 ], pair.Value[ 1 ] );
}
}
else
{
var deleted = Enumerable.Repeat( false, pair.Value.Count ).ToArray();
var hashes = pair.Value.Select( dedup.ComputeHash ).ToArray();
for( var i = 0; i < pair.Value.Count; ++i )
{
if( deleted[ i ] )
{
continue;
}
for( var j = i + 1; j < pair.Value.Count; ++j )
{
if( deleted[ j ] || !CompareHashes( hashes[ i ], hashes[ j ] ) )
{
continue;
}
dedup.ReplaceFile( pair.Value[ i ], pair.Value[ j ] );
deleted[ j ] = true;
}
}
}
}
CleanUpDuplicates( mod );
ClearEmptySubDirectories( dedup._baseDir );
}
private void ReplaceFile( FileInfo f1, FileInfo f2 )
{
RelPath relName1 = new( f1, _baseDir );
RelPath relName2 = new( f2, _baseDir );
var inOption1 = false;
var inOption2 = false;
foreach( var option in _mod.Groups.SelectMany( g => g.Value.Options ) )
{
if( option.OptionFiles.ContainsKey( relName1 ) )
{
inOption1 = true;
}
if( !option.OptionFiles.TryGetValue( relName2, out var values ) )
{
continue;
}
inOption2 = true;
foreach( var value in values )
{
option.AddFile( relName1, value );
}
option.OptionFiles.Remove( relName2 );
}
if( !inOption1 || !inOption2 )
{
var duplicates = FindOrCreateDuplicates( _mod );
if( !inOption1 )
{
duplicates.AddFile( relName1, new GamePath( relName2, 0 ) );
}
if( !inOption2 )
{
duplicates.AddFile( relName1, new GamePath( relName1, 0 ) );
}
}
PluginLog.Information( $"File {relName1} and {relName2} are identical. Deleting the second." );
f2.Delete();
}
public static bool CompareFilesDirectly( FileInfo f1, FileInfo f2 )
=> File.ReadAllBytes( f1.FullName ).SequenceEqual( File.ReadAllBytes( f2.FullName ) );
public static bool CompareHashes( byte[] f1, byte[] f2 )
=> StructuralComparisons.StructuralEqualityComparer.Equals( f1, f2 );
public byte[] ComputeHash( FileInfo f )
{
var stream = File.OpenRead( f.FullName );
var ret = Sha().ComputeHash( stream );
stream.Dispose();
return ret;
}
// Does not delete the base directory itself even if it is completely empty at the end.
public static void ClearEmptySubDirectories( DirectoryInfo baseDir )
{
foreach( var subDir in baseDir.GetDirectories() )
{
ClearEmptySubDirectories( subDir );
if( subDir.GetFiles().Length == 0 && subDir.GetDirectories().Length == 0 )
{
subDir.Delete();
}
}
}
private static bool FileIsInAnyGroup( ModMeta meta, RelPath relPath, bool exceptDuplicates = false )
{
var groupEnumerator = exceptDuplicates
? meta.Groups.Values.Where( g => g.GroupName != Duplicates )
: meta.Groups.Values;
return groupEnumerator.SelectMany( group => group.Options )
.Any( option => option.OptionFiles.ContainsKey( relPath ) );
}
private static void CleanUpDuplicates( ModMeta meta )
{
if( !meta.Groups.TryGetValue( Duplicates, out var info ) )
{
return;
}
var requiredIdx = info.Options.FindIndex( o => o.OptionName == Required );
if( requiredIdx >= 0 )
{
var required = info.Options[ requiredIdx ];
foreach( var kvp in required.OptionFiles.ToArray() )
{
if( kvp.Value.Count > 1 || FileIsInAnyGroup( meta, kvp.Key, true ) )
{
continue;
}
if( kvp.Value.Count == 0 || kvp.Value.First().CompareTo( new GamePath( kvp.Key, 0 ) ) == 0 )
{
required.OptionFiles.Remove( kvp.Key );
}
}
if( required.OptionFiles.Count == 0 )
{
info.Options.RemoveAt( requiredIdx );
}
}
if( info.Options.Count == 0 )
{
meta.Groups.Remove( Duplicates );
}
}
public enum GroupType
{
Both = 0,
Single = 1,
Multi = 2,
};
private static void RemoveFromGroups( ModMeta meta, RelPath relPath, GamePath gamePath, GroupType type = GroupType.Both,
bool skipDuplicates = true )
{
if( meta.Groups.Count == 0 )
{
return;
}
var enumerator = type switch
{
GroupType.Both => meta.Groups.Values,
GroupType.Single => meta.Groups.Values.Where( g => g.SelectionType == SelectType.Single ),
GroupType.Multi => meta.Groups.Values.Where( g => g.SelectionType == SelectType.Multi ),
_ => throw new InvalidEnumArgumentException( "Invalid Enum in RemoveFromGroups" ),
};
foreach( var group in enumerator )
{
var optionEnum = skipDuplicates
? group.Options.Where( o => group.GroupName != Duplicates || o.OptionName != Required )
: group.Options;
foreach( var option in optionEnum )
{
if( option.OptionFiles.TryGetValue( relPath, out var gamePaths ) && gamePaths.Remove( gamePath ) && gamePaths.Count == 0 )
{
option.OptionFiles.Remove( relPath );
}
}
}
}
public static bool MoveFile( ModMeta meta, string basePath, RelPath oldRelPath, RelPath newRelPath )
{
if( oldRelPath == newRelPath )
{
return true;
}
try
{
var newFullPath = Path.Combine( basePath, newRelPath );
new FileInfo( newFullPath ).Directory!.Create();
File.Move( Path.Combine( basePath, oldRelPath ), newFullPath );
}
catch( Exception e )
{
PluginLog.Error( $"Could not move file from {oldRelPath} to {newRelPath}:\n{e}" );
return false;
}
foreach( var option in meta.Groups.Values.SelectMany( group => group.Options ) )
{
if( option.OptionFiles.TryGetValue( oldRelPath, out var gamePaths ) )
{
option.OptionFiles.Add( newRelPath, gamePaths );
option.OptionFiles.Remove( oldRelPath );
}
}
return true;
}
private static void RemoveUselessGroups( ModMeta meta )
{
meta.Groups = meta.Groups.Where( kvp => kvp.Value.Options.Any( o => o.OptionFiles.Count > 0 ) )
.ToDictionary( kvp => kvp.Key, kvp => kvp.Value );
}
// Goes through all Single-Select options and checks if file links are in each of them.
// If they are, it moves those files to the root folder and removes them from the groups (and puts them to duplicates, if necessary).
public static void Normalize( DirectoryInfo baseDir, ModMeta meta )
{
foreach( var group in meta.Groups.Values.Where( g => g.SelectionType == SelectType.Single && g.GroupName != Duplicates ) )
{
var firstOption = true;
HashSet< (RelPath, GamePath) > groupList = new();
foreach( var option in group.Options )
{
HashSet< (RelPath, GamePath) > optionList = new();
foreach( var (file, gamePaths) in option.OptionFiles.Select( p => ( p.Key, p.Value ) ) )
{
optionList.UnionWith( gamePaths.Select( p => ( file, p ) ) );
}
if( firstOption )
{
groupList = optionList;
}
else
{
groupList.IntersectWith( optionList );
}
firstOption = false;
}
var newPath = new Dictionary< RelPath, GamePath >();
foreach( var (path, gamePath) in groupList )
{
var relPath = new RelPath( gamePath );
if( newPath.TryGetValue( path, out var usedGamePath ) )
{
var required = FindOrCreateDuplicates( meta );
var usedRelPath = new RelPath( usedGamePath );
required.AddFile( usedRelPath, gamePath );
required.AddFile( usedRelPath, usedGamePath );
RemoveFromGroups( meta, relPath, gamePath, GroupType.Single );
}
else if( MoveFile( meta, baseDir.FullName, path, relPath ) )
{
newPath[ path ] = gamePath;
if( FileIsInAnyGroup( meta, relPath ) )
{
FindOrCreateDuplicates( meta ).AddFile( relPath, gamePath );
}
RemoveFromGroups( meta, relPath, gamePath, GroupType.Single );
}
}
}
RemoveUselessGroups( meta );
ClearEmptySubDirectories( baseDir );
}
}
}

59
Penumbra/Mod/ModData.cs Normal file
View file

@ -0,0 +1,59 @@
using System.IO;
using Dalamud.Plugin;
namespace Penumbra.Mod
{
public class ModData
{
public DirectoryInfo BasePath;
public ModMeta Meta;
public ModResources Resources;
public FileInfo MetaFile { get; set; }
private ModData( DirectoryInfo basePath, ModMeta meta, ModResources resources )
{
BasePath = basePath;
Meta = meta;
Resources = resources;
MetaFile = MetaFileInfo( basePath );
}
public static FileInfo MetaFileInfo( DirectoryInfo basePath )
=> new( Path.Combine( basePath.FullName, "meta.json" ) );
public static ModData? LoadMod( DirectoryInfo basePath )
{
basePath.Refresh();
if( !basePath.Exists )
{
PluginLog.Error( $"Supplied mod directory {basePath} does not exist." );
return null;
}
var metaFile = MetaFileInfo( basePath );
if( !metaFile.Exists )
{
PluginLog.Debug( "No mod meta found for {ModLocation}.", basePath.Name );
return null;
}
var meta = ModMeta.LoadFromFile( metaFile );
if( meta == null )
{
return null;
}
var data = new ModResources();
if( data.RefreshModFiles( basePath ).HasFlag( ResourceChange.Meta ) )
{
data.SetManipulations( meta, basePath );
}
return new ModData( basePath, meta, data );
}
public void SaveMeta()
=> Meta.SaveToFile( MetaFile );
}
}

View file

@ -0,0 +1,82 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Penumbra.Structs;
using Penumbra.Util;
namespace Penumbra.Mod
{
public static class ModFunctions
{
public static bool CleanUpCollection( Dictionary< string, ModSettings > settings, IEnumerable< DirectoryInfo > modPaths )
{
var hashes = modPaths.Select( p => p.Name ).ToHashSet();
var missingMods = settings.Keys.Where( k => !hashes.Contains( k ) ).ToArray();
var anyChanges = false;
foreach( var toRemove in missingMods )
{
anyChanges |= settings.Remove( toRemove );
}
return anyChanges;
}
public static HashSet< GamePath > GetFilesForConfig( RelPath relPath, ModSettings settings, ModMeta meta )
{
var doNotAdd = false;
var files = new HashSet< GamePath >();
foreach( var group in meta.Groups.Values.Where( g => g.Options.Count > 0 ) )
{
doNotAdd |= group.ApplyGroupFiles( relPath, settings.Settings[ group.GroupName ], files );
}
if( !doNotAdd )
{
files.Add( new GamePath( relPath ) );
}
return files;
}
public static ModSettings ConvertNamedSettings( NamedModSettings namedSettings, ModMeta meta )
{
ModSettings ret = new()
{
Priority = namedSettings.Priority,
Settings = namedSettings.Settings.Keys.ToDictionary( k => k, _ => 0 ),
};
foreach( var kvp in namedSettings.Settings )
{
if( !meta.Groups.TryGetValue( kvp.Key, out var info ) )
{
continue;
}
if( info.SelectionType == SelectType.Single )
{
if( namedSettings.Settings[ kvp.Key ].Count == 0 )
{
ret.Settings[ kvp.Key ] = 0;
}
else
{
var idx = info.Options.FindIndex( o => o.OptionName == namedSettings.Settings[ kvp.Key ].Last() );
ret.Settings[ kvp.Key ] = idx < 0 ? 0 : idx;
}
}
else
{
foreach( var idx in namedSettings.Settings[ kvp.Key ]
.Select( option => info.Options.FindIndex( o => o.OptionName == option ) )
.Where( idx => idx >= 0 ) )
{
ret.Settings[ kvp.Key ] |= 1 << idx;
}
}
}
return ret;
}
}
}

103
Penumbra/Mod/ModMeta.cs Normal file
View file

@ -0,0 +1,103 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dalamud.Plugin;
using Newtonsoft.Json;
using Penumbra.Structs;
using Penumbra.Util;
namespace Penumbra.Mod
{
// Contains descriptive data about the mod as well as possible settings and fileswaps.
public class ModMeta
{
public uint FileVersion { get; set; }
public string Name { get; set; } = "Mod";
public string Author { get; set; } = "";
public string Description { get; set; } = "";
public string Version { get; set; } = "";
public string Website { get; set; } = "";
public List< string > ChangedItems { get; set; } = new();
[JsonProperty( ItemConverterType = typeof( GamePathConverter ) )]
public Dictionary< GamePath, GamePath > FileSwaps { get; set; } = new();
public Dictionary< string, OptionGroup > Groups { get; set; } = new();
[JsonIgnore]
private int FileHash { get; set; }
[JsonIgnore]
public bool HasGroupsWithConfig { get; private set; }
public bool RefreshFromFile( FileInfo filePath )
{
var newMeta = LoadFromFile( filePath );
if( newMeta == null )
{
return true;
}
if( newMeta.FileHash == FileHash )
{
return false;
}
FileVersion = newMeta.FileVersion;
Name = newMeta.Name;
Author = newMeta.Author;
Description = newMeta.Description;
Version = newMeta.Version;
Website = newMeta.Website;
ChangedItems = newMeta.ChangedItems;
FileSwaps = newMeta.FileSwaps;
Groups = newMeta.Groups;
FileHash = newMeta.FileHash;
HasGroupsWithConfig = newMeta.HasGroupsWithConfig;
return true;
}
public static ModMeta? LoadFromFile( FileInfo filePath )
{
try
{
var text = File.ReadAllText( filePath.FullName );
var meta = JsonConvert.DeserializeObject< ModMeta >( text,
new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore } );
if( meta != null )
{
meta.FileHash = text.GetHashCode();
meta.HasGroupsWithConfig = meta.Groups.Values.Any( g => g.SelectionType == SelectType.Multi || g.Options.Count > 1 );
}
return meta;
}
catch( Exception e )
{
PluginLog.Error( $"Could not load mod meta:\n{e}" );
return null;
}
}
public void SaveToFile( FileInfo filePath )
{
try
{
var text = JsonConvert.SerializeObject( this, Formatting.Indented );
var newHash = text.GetHashCode();
if( newHash != FileHash )
{
File.WriteAllText( filePath.FullName, text );
FileHash = newHash;
}
}
catch( Exception e )
{
PluginLog.Error( $"Could not write meta file for mod {Name} to {filePath.FullName}:\n{e}" );
}
}
}
}

View file

@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Penumbra.Meta;
namespace Penumbra.Mod
{
[Flags]
public enum ResourceChange
{
Files = 1,
Meta = 2,
}
// Contains static mod data that should only change on filesystem changes.
public class ModResources
{
public List< FileInfo > ModFiles { get; private set; } = new();
public List< FileInfo > MetaFiles { get; private set; } = new();
public MetaCollection MetaManipulations { get; private set; } = new();
private void ForceManipulationsUpdate( ModMeta meta, DirectoryInfo basePath )
{
MetaManipulations.Update( MetaFiles, basePath, meta );
MetaManipulations.SaveToFile( MetaCollection.FileName( basePath ) );
}
public void SetManipulations( ModMeta meta, DirectoryInfo basePath )
{
var newManipulations = MetaCollection.LoadFromFile( MetaCollection.FileName( basePath ) );
if( newManipulations == null )
{
ForceManipulationsUpdate( meta, basePath );
}
else
{
MetaManipulations = newManipulations;
if( !MetaManipulations.Validate( meta ) )
{
ForceManipulationsUpdate( meta, basePath );
}
}
}
// Update the current set of files used by the mod,
// returns true if anything changed.
public ResourceChange RefreshModFiles( DirectoryInfo basePath )
{
List< FileInfo > tmpFiles = new( ModFiles.Count );
List< FileInfo > tmpMetas = new( MetaFiles.Count );
// we don't care about any _files_ in the root dir, but any folders should be a game folder/file combo
foreach( var file in basePath.EnumerateDirectories()
.SelectMany( dir => dir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) )
.OrderBy( f => f.FullName ) )
{
if( file.Extension != ".meta" )
{
tmpFiles.Add( file );
}
else
{
tmpMetas.Add( file );
}
}
ResourceChange changes = 0;
if( !tmpFiles.SequenceEqual( ModFiles ) )
{
ModFiles = tmpFiles;
changes |= ResourceChange.Files;
}
if( !tmpMetas.SequenceEqual( MetaFiles ) )
{
MetaFiles = tmpMetas;
changes |= ResourceChange.Meta;
}
return changes;
}
}
}

View file

@ -0,0 +1,74 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Penumbra.Structs;
namespace Penumbra.Mod
{
public class ModSettings
{
public bool Enabled { get; set; }
public int Priority { get; set; }
public Dictionary< string, int > Settings { get; set; } = new();
// For backwards compatibility
private Dictionary< string, int > Conf
{
set => Settings = value;
}
public ModSettings DeepCopy()
{
var settings = new ModSettings
{
Enabled = Enabled,
Priority = Priority,
Settings = Settings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value ),
};
return settings;
}
public static ModSettings DefaultSettings( ModMeta meta )
{
return new()
{
Enabled = false,
Priority = 0,
Settings = meta.Groups.ToDictionary( kvp => kvp.Key, _ => 0 ),
};
}
public bool FixSpecificSetting( string name, ModMeta meta )
{
if( !meta.Groups.TryGetValue( name, out var group ) )
{
return Settings.Remove( name );
}
if( Settings.TryGetValue( name, out var oldSetting ) )
{
Settings[ name ] = group.SelectionType switch
{
SelectType.Single => Math.Min( Math.Max( oldSetting, 0 ), group.Options.Count - 1 ),
SelectType.Multi => Math.Min( Math.Max( oldSetting, 0 ), ( 1 << group.Options.Count ) - 1 ),
_ => Settings[ group.GroupName ],
};
return oldSetting != Settings[ group.GroupName ];
}
Settings[ name ] = 0;
return true;
}
public bool FixInvalidSettings( ModMeta meta )
{
if( meta.Groups.Count == 0 )
{
return false;
}
return Settings.Keys.ToArray().Union( meta.Groups.Keys )
.Aggregate( false, ( current, name ) => current | FixSpecificSetting( name, meta ) );
}
}
}

View file

@ -0,0 +1,45 @@
using System.Collections.Generic;
using System.Linq;
using Penumbra.Structs;
namespace Penumbra.Mod
{
public class NamedModSettings
{
public int Priority { get; set; }
public Dictionary< string, HashSet< string > > Settings { get; set; } = new();
public void AddFromModSetting( ModSettings s, ModMeta meta )
{
Priority = s.Priority;
Settings = s.Settings.Keys.ToDictionary( k => k, _ => new HashSet< string >() );
foreach( var kvp in Settings )
{
if( !meta.Groups.TryGetValue( kvp.Key, out var info ) )
{
continue;
}
var setting = s.Settings[ kvp.Key ];
if( info.SelectionType == SelectType.Single )
{
var name = setting < info.Options.Count
? info.Options[ setting ].OptionName
: info.Options[ 0 ].OptionName;
kvp.Value.Add( name );
}
else
{
for( var i = 0; i < info.Options.Count; ++i )
{
if( ( ( setting >> i ) & 1 ) != 0 )
{
kvp.Value.Add( info.Options[ i ].OptionName );
}
}
}
}
}
}
}