Fix exception with empty option groups.

This commit is contained in:
Ottermandias 2024-01-29 13:27:05 +01:00
parent 4610686a70
commit a5f0c2f943
2 changed files with 87 additions and 95 deletions

View file

@ -11,9 +11,9 @@ namespace Penumbra.Mods.Subclasses;
public class ModSettings public class ModSettings
{ {
public static readonly ModSettings Empty = new(); public static readonly ModSettings Empty = new();
public List< uint > Settings { get; private init; } = []; public List<uint> Settings { get; private init; } = [];
public int Priority { get; set; } public int Priority { get; set; }
public bool Enabled { get; set; } public bool Enabled { get; set; }
// Create an independent copy of the current settings. // Create an independent copy of the current settings.
public ModSettings DeepCopy() public ModSettings DeepCopy()
@ -25,148 +25,143 @@ public class ModSettings
}; };
// Create default settings for a given mod. // Create default settings for a given mod.
public static ModSettings DefaultSettings( Mod mod ) public static ModSettings DefaultSettings(Mod mod)
=> new() => new()
{ {
Enabled = false, Enabled = false,
Priority = 0, Priority = 0,
Settings = mod.Groups.Select( g => g.DefaultSettings ).ToList(), Settings = mod.Groups.Select(g => g.DefaultSettings).ToList(),
}; };
// Return everything required to resolve things for a single mod with given settings (which can be null, in which case the default is used. // Return everything required to resolve things for a single mod with given settings (which can be null, in which case the default is used.
public static (Dictionary< Utf8GamePath, FullPath >, HashSet< MetaManipulation >) GetResolveData( Mod mod, ModSettings? settings ) public static (Dictionary<Utf8GamePath, FullPath>, HashSet<MetaManipulation>) GetResolveData(Mod mod, ModSettings? settings)
{ {
if (settings == null) if (settings == null)
settings = DefaultSettings(mod); settings = DefaultSettings(mod);
else else
settings.AddMissingSettings( mod ); settings.AddMissingSettings(mod);
var dict = new Dictionary< Utf8GamePath, FullPath >(); var dict = new Dictionary<Utf8GamePath, FullPath>();
var set = new HashSet< MetaManipulation >(); var set = new HashSet<MetaManipulation>();
foreach( var (group, index) in mod.Groups.WithIndex().OrderByDescending( g => g.Value.Priority ) ) foreach (var (group, index) in mod.Groups.WithIndex().OrderByDescending(g => g.Value.Priority))
{ {
if( group.Type is GroupType.Single ) if (group.Type is GroupType.Single)
{ {
AddOption( group[ ( int )settings.Settings[ index ] ] ); if (group.Count > 0)
AddOption(group[(int)settings.Settings[index]]);
} }
else else
{ {
foreach( var (option, optionIdx) in group.WithIndex().OrderByDescending( o => group.OptionPriority( o.Index ) ) ) foreach (var (option, optionIdx) in group.WithIndex().OrderByDescending(o => group.OptionPriority(o.Index)))
{ {
if( ( ( settings.Settings[ index ] >> optionIdx ) & 1 ) == 1 ) if (((settings.Settings[index] >> optionIdx) & 1) == 1)
{ AddOption(option);
AddOption( option );
}
} }
} }
} }
AddOption( mod.Default ); AddOption(mod.Default);
return ( dict, set ); return (dict, set);
void AddOption( ISubMod option ) void AddOption(ISubMod option)
{ {
foreach( var (path, file) in option.Files.Concat( option.FileSwaps ) ) foreach (var (path, file) in option.Files.Concat(option.FileSwaps))
{ dict.TryAdd(path, file);
dict.TryAdd( path, file );
}
foreach( var manip in option.Manipulations ) foreach (var manip in option.Manipulations)
{ set.Add(manip);
set.Add( manip );
}
} }
} }
// Automatically react to changes in a mods available options. // Automatically react to changes in a mods available options.
public bool HandleChanges( ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx ) public bool HandleChanges(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx)
{ {
switch( type ) switch (type)
{ {
case ModOptionChangeType.GroupRenamed: return true; case ModOptionChangeType.GroupRenamed: return true;
case ModOptionChangeType.GroupAdded: case ModOptionChangeType.GroupAdded:
// Add new empty setting for new mod. // Add new empty setting for new mod.
Settings.Insert( groupIdx, mod.Groups[ groupIdx ].DefaultSettings ); Settings.Insert(groupIdx, mod.Groups[groupIdx].DefaultSettings);
return true; return true;
case ModOptionChangeType.GroupDeleted: case ModOptionChangeType.GroupDeleted:
// Remove setting for deleted mod. // Remove setting for deleted mod.
Settings.RemoveAt( groupIdx ); Settings.RemoveAt(groupIdx);
return true; return true;
case ModOptionChangeType.GroupTypeChanged: case ModOptionChangeType.GroupTypeChanged:
{ {
// Fix settings for a changed group type. // Fix settings for a changed group type.
// Single -> Multi: set single as enabled, rest as disabled // Single -> Multi: set single as enabled, rest as disabled
// Multi -> Single: set the first enabled option or 0. // Multi -> Single: set the first enabled option or 0.
var group = mod.Groups[ groupIdx ]; var group = mod.Groups[groupIdx];
var config = Settings[ groupIdx ]; var config = Settings[groupIdx];
Settings[ groupIdx ] = group.Type switch Settings[groupIdx] = group.Type switch
{ {
GroupType.Single => ( uint )Math.Max( Math.Min( group.Count - 1, BitOperations.TrailingZeroCount( config ) ), 0 ), GroupType.Single => (uint)Math.Max(Math.Min(group.Count - 1, BitOperations.TrailingZeroCount(config)), 0),
GroupType.Multi => 1u << ( int )config, GroupType.Multi => 1u << (int)config,
_ => config, _ => config,
}; };
return config != Settings[ groupIdx ]; return config != Settings[groupIdx];
} }
case ModOptionChangeType.OptionDeleted: case ModOptionChangeType.OptionDeleted:
{ {
// Single -> select the previous option if any. // Single -> select the previous option if any.
// Multi -> excise the corresponding bit. // Multi -> excise the corresponding bit.
var group = mod.Groups[ groupIdx ]; var group = mod.Groups[groupIdx];
var config = Settings[ groupIdx ]; var config = Settings[groupIdx];
Settings[ groupIdx ] = group.Type switch Settings[groupIdx] = group.Type switch
{ {
GroupType.Single => config >= optionIdx ? config > 1 ? config - 1 : 0 : config, GroupType.Single => config >= optionIdx ? config > 1 ? config - 1 : 0 : config,
GroupType.Multi => Functions.RemoveBit( config, optionIdx ), GroupType.Multi => Functions.RemoveBit(config, optionIdx),
_ => config, _ => config,
}; };
return config != Settings[ groupIdx ]; return config != Settings[groupIdx];
} }
case ModOptionChangeType.GroupMoved: case ModOptionChangeType.GroupMoved:
// Move the group the same way. // Move the group the same way.
return Settings.Move( groupIdx, movedToIdx ); return Settings.Move(groupIdx, movedToIdx);
case ModOptionChangeType.OptionMoved: case ModOptionChangeType.OptionMoved:
{ {
// Single -> select the moved option if it was currently selected // Single -> select the moved option if it was currently selected
// Multi -> move the corresponding bit // Multi -> move the corresponding bit
var group = mod.Groups[ groupIdx ]; var group = mod.Groups[groupIdx];
var config = Settings[ groupIdx ]; var config = Settings[groupIdx];
Settings[ groupIdx ] = group.Type switch Settings[groupIdx] = group.Type switch
{ {
GroupType.Single => config == optionIdx ? ( uint )movedToIdx : config, GroupType.Single => config == optionIdx ? (uint)movedToIdx : config,
GroupType.Multi => Functions.MoveBit( config, optionIdx, movedToIdx ), GroupType.Multi => Functions.MoveBit(config, optionIdx, movedToIdx),
_ => config, _ => config,
}; };
return config != Settings[ groupIdx ]; return config != Settings[groupIdx];
} }
default: return false; default: return false;
} }
} }
// Ensure that a value is valid for a group. // Ensure that a value is valid for a group.
private static uint FixSetting( IModGroup group, uint value ) private static uint FixSetting(IModGroup group, uint value)
=> group.Type switch => group.Type switch
{ {
GroupType.Single => ( uint )Math.Min( value, group.Count - 1 ), GroupType.Single => (uint)Math.Min(value, group.Count - 1),
GroupType.Multi => ( uint )( value & ( ( 1ul << group.Count ) - 1 ) ), GroupType.Multi => (uint)(value & ((1ul << group.Count) - 1)),
_ => value, _ => value,
}; };
// Set a setting. Ensures that there are enough settings and fixes the setting beforehand. // Set a setting. Ensures that there are enough settings and fixes the setting beforehand.
public void SetValue( Mod mod, int groupIdx, uint newValue ) public void SetValue(Mod mod, int groupIdx, uint newValue)
{ {
AddMissingSettings( mod ); AddMissingSettings(mod);
var group = mod.Groups[ groupIdx ]; var group = mod.Groups[groupIdx];
Settings[ groupIdx ] = FixSetting( group, newValue ); Settings[groupIdx] = FixSetting(group, newValue);
} }
// Add defaulted settings up to the required count. // Add defaulted settings up to the required count.
private bool AddMissingSettings( Mod mod ) private bool AddMissingSettings(Mod mod)
{ {
var changes = false; var changes = false;
for( var i = Settings.Count; i < mod.Groups.Count; ++i ) for (var i = Settings.Count; i < mod.Groups.Count; ++i)
{ {
Settings.Add( mod.Groups[ i ].DefaultSettings ); Settings.Add(mod.Groups[i].DefaultSettings);
changes = true; changes = true;
} }
@ -176,51 +171,47 @@ public class ModSettings
// A simple struct conversion to easily save settings by name instead of value. // A simple struct conversion to easily save settings by name instead of value.
public struct SavedSettings public struct SavedSettings
{ {
public Dictionary< string, long > Settings; public Dictionary<string, long> Settings;
public int Priority; public int Priority;
public bool Enabled; public bool Enabled;
public SavedSettings DeepCopy() public SavedSettings DeepCopy()
=> new() => new()
{ {
Enabled = Enabled, Enabled = Enabled,
Priority = Priority, Priority = Priority,
Settings = Settings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value ), Settings = Settings.ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
}; };
public SavedSettings( ModSettings settings, Mod mod ) public SavedSettings(ModSettings settings, Mod mod)
{ {
Priority = settings.Priority; Priority = settings.Priority;
Enabled = settings.Enabled; Enabled = settings.Enabled;
Settings = new Dictionary< string, long >( mod.Groups.Count ); Settings = new Dictionary<string, long>(mod.Groups.Count);
settings.AddMissingSettings( mod ); settings.AddMissingSettings(mod);
foreach( var (group, setting) in mod.Groups.Zip( settings.Settings ) ) foreach (var (group, setting) in mod.Groups.Zip(settings.Settings))
{ Settings.Add(group.Name, setting);
Settings.Add( group.Name, setting );
}
} }
// Convert and fix. // Convert and fix.
public bool ToSettings( Mod mod, out ModSettings settings ) public bool ToSettings(Mod mod, out ModSettings settings)
{ {
var list = new List< uint >( mod.Groups.Count ); var list = new List<uint>(mod.Groups.Count);
var changes = Settings.Count != mod.Groups.Count; var changes = Settings.Count != mod.Groups.Count;
foreach( var group in mod.Groups ) foreach (var group in mod.Groups)
{ {
if( Settings.TryGetValue( group.Name, out var config ) ) if (Settings.TryGetValue(group.Name, out var config))
{ {
var castConfig = ( uint )Math.Clamp( config, 0, uint.MaxValue ); var castConfig = (uint)Math.Clamp(config, 0, uint.MaxValue);
var actualConfig = FixSetting( group, castConfig ); var actualConfig = FixSetting(group, castConfig);
list.Add( actualConfig ); list.Add(actualConfig);
if( actualConfig != config ) if (actualConfig != config)
{
changes = true; changes = true;
}
} }
else else
{ {
list.Add( 0 ); list.Add(0);
changes = true; changes = true;
} }
} }
@ -238,28 +229,29 @@ public class ModSettings
// Return the settings for a given mod in a shareable format, using the names of groups and options instead of indices. // Return the settings for a given mod in a shareable format, using the names of groups and options instead of indices.
// Does not repair settings but ignores settings not fitting to the given mod. // Does not repair settings but ignores settings not fitting to the given mod.
public (bool Enabled, int Priority, Dictionary< string, IList< string > > Settings) ConvertToShareable( Mod mod ) public (bool Enabled, int Priority, Dictionary<string, IList<string>> Settings) ConvertToShareable(Mod mod)
{ {
var dict = new Dictionary< string, IList< string > >( Settings.Count ); var dict = new Dictionary<string, IList<string>>(Settings.Count);
foreach( var (setting, idx) in Settings.WithIndex() ) foreach (var (setting, idx) in Settings.WithIndex())
{ {
if( idx >= mod.Groups.Count ) if (idx >= mod.Groups.Count)
{
break; break;
}
var group = mod.Groups[ idx ]; var group = mod.Groups[idx];
if( group.Type == GroupType.Single && setting < group.Count ) if (group.Type == GroupType.Single && setting < group.Count)
{ {
dict.Add( group.Name, new[] { group[ ( int )setting ].Name } ); dict.Add(group.Name, new[]
{
group[(int)setting].Name,
});
} }
else else
{ {
var list = group.Where( ( _, optionIdx ) => ( setting & ( 1 << optionIdx ) ) != 0 ).Select( o => o.Name ).ToList(); var list = group.Where((_, optionIdx) => (setting & (1 << optionIdx)) != 0).Select(o => o.Name).ToList();
dict.Add( group.Name, list ); dict.Add(group.Name, list);
} }
} }
return ( Enabled, Priority, dict ); return (Enabled, Priority, dict);
} }
} }

View file

@ -17,7 +17,7 @@ public sealed class SingleModGroup : IModGroup
public int Priority { get; set; } public int Priority { get; set; }
public uint DefaultSettings { get; set; } public uint DefaultSettings { get; set; }
public readonly List<SubMod> OptionData = new(); public readonly List<SubMod> OptionData = [];
public int OptionPriority(Index _) public int OptionPriority(Index _)
=> Priority; => Priority;