Adding support for mod groups/options. Removed SwapFiles.

This commit is contained in:
Ottermandias 2021-01-14 14:48:46 +01:00
parent c472fdd8cf
commit 01215b5697
9 changed files with 284 additions and 80 deletions

View file

@ -204,11 +204,6 @@ namespace Penumbra.Importer
);
newModFolder.Create();
File.WriteAllText(
Path.Combine( newModFolder.FullName, "meta.json" ),
JsonConvert.SerializeObject( modMeta )
);
if( modList.SimpleModsList != null )
ExtractSimpleModList( newModFolder, modList.SimpleModsList, modData );
@ -216,17 +211,36 @@ namespace Penumbra.Importer
return;
// Iterate through all pages
// For now, we are just going to import the default selections
// TODO: implement such a system in resrep?
foreach( var option in from modPackPage in modList.ModPackPages
from modGroup in modPackPage.ModGroups
from option in modGroup.OptionList
where option.IsChecked
select option )
{
var OptionFolder = new DirectoryInfo(Path.Combine(newModFolder.FullName, option.Name));
ExtractSimpleModList(OptionFolder, option.ModsJsons, modData );
AddMeta(OptionFolder, newModFolder, modMeta, option.Name);
}
File.WriteAllText(
Path.Combine( newModFolder.FullName, "meta.json" ),
JsonConvert.SerializeObject( modMeta, Formatting.Indented )
);
}
void AddMeta(DirectoryInfo optionFolder, DirectoryInfo baseFolder, ModMeta meta, string optionName)
{
var optionFolderLength = optionFolder.FullName.Length;
var baseFolderLength = baseFolder.FullName.Length;
foreach( var dir in optionFolder.EnumerateDirectories() )
{
ExtractSimpleModList( newModFolder, option.ModsJsons, modData );
}
}
foreach( var file in dir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) )
{
meta.Groups.AddFileToOtherGroups(optionName
, file.FullName.Substring(baseFolderLength).TrimStart('\\')
, file.FullName.Substring(optionFolderLength).TrimStart('\\').Replace('\\', '/'));
}
}
}
private void ImportMetaModPack( FileInfo file )
{

View file

@ -0,0 +1,157 @@
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using System.Linq;
namespace Penumbra.Models
{
[Serializable]
public class GroupInformation : ISerializable
{
// This class is just used as a temp class while (de)-serializing.
// It converts the flags into lists and back.
[Serializable]
private class GroupDescription : ISerializable
{
public GroupDescription(GroupInformation info, (string, uint, uint, ulong) vars)
{
GamePath = vars.Item1;
static List<string> AddGroupTypes(ulong flags, ulong bound, List<string> groupType)
{
List<string> ret = null;
if (flags != uint.MaxValue)
{
ret = new();
for (var i = 0; i < groupType.Count; ++i)
{
var flag = 1u << i;
if ((flags & flag) == flag)
ret.Add(groupType[i]);
}
}
return ret;
}
// Tops and Bottoms are uint.
TopTypes = AddGroupTypes(vars.Item2, uint.MaxValue, info.TopTypes);
BottomTypes = AddGroupTypes(vars.Item3, uint.MaxValue, info.BottomTypes);
// Exclusions are the other way around and ulong.
GroupExclusions = AddGroupTypes(~vars.Item4, 0, info.OtherGroups);
}
public (string, uint, uint, ulong) ToTuple(GroupInformation info)
{
static ulong TypesToFlags(List<string> ownTypes, List<string> globalTypes)
{
if (ownTypes == null)
return ulong.MaxValue;
ulong flags = 0;
foreach (var x in ownTypes)
{
var index = globalTypes.IndexOf(x);
if (index >= 0)
flags |= (1u << index);
}
return flags;
}
var tops = (uint) TypesToFlags(TopTypes, info.TopTypes);
var bottoms = (uint) TypesToFlags(BottomTypes, info.BottomTypes);
// Exclusions are the other way around.
var groupEx = (GroupExclusions == null) ? ulong.MaxValue : ~TypesToFlags(GroupExclusions, info.OtherGroups);
return (GamePath, tops, bottoms, groupEx);
}
public string GamePath { get; set; }
public List<string> TopTypes { get; set; } = null;
public List<string> BottomTypes { get; set; } = null;
public List<string> GroupExclusions { get; set; } = null;
// Customize (De)-Serialization to ignore nulls.
public GroupDescription(SerializationInfo info, StreamingContext context)
{
List<string> readListOrNull(string name)
{
try
{
var ret = (List<string>) info.GetValue(name, typeof(List<string>));
if (ret == null || ret.Count == 0)
return null;
return ret;
}
catch (Exception) { return null; }
}
GamePath = info.GetString("GamePath");
TopTypes = readListOrNull("TopTypes");
BottomTypes = readListOrNull("BottomTypes");
GroupExclusions = readListOrNull("GroupExclusions");
}
public virtual void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.AddValue( "GamePath", GamePath );
if (TopTypes != null) info.AddValue("TopTypes", TopTypes);
if (BottomTypes != null) info.AddValue("BottomTypes", BottomTypes);
if (GroupExclusions != null) info.AddValue("GroupExclusions", GroupExclusions);
}
}
public List<string> TopTypes { get; set; } = new();
public List<string> BottomTypes { get; set; } = new();
public List<string> OtherGroups { get; set; } = new();
public void AddFileToOtherGroups(string optionName, string fileName, string gamePath)
{
var idx = OtherGroups.IndexOf(optionName);
if (idx < 0)
{
idx = OtherGroups.Count;
OtherGroups.Add(optionName);
}
(string, uint, uint, ulong) tuple = (gamePath, uint.MaxValue, uint.MaxValue, (1ul << idx));
if (!FileToGameAndGroup.TryGetValue(fileName, out var tuple2))
{
FileToGameAndGroup.Add(fileName, tuple);
}
else
{
tuple2.Item1 = tuple.Item1;
tuple2.Item4 |= tuple.Item4;
}
}
public Dictionary<string, (string, uint, uint, ulong)> FileToGameAndGroup { get; set; } = new();
public GroupInformation(){ }
public GroupInformation(SerializationInfo info, StreamingContext context)
{
try { TopTypes = (List<string>) info.GetValue( "TopTypes", TopTypes.GetType() ); } catch(Exception){ }
try { BottomTypes = (List<string>) info.GetValue( "BottomTypes", BottomTypes.GetType() ); } catch(Exception){ }
try { OtherGroups = (List<string>) info.GetValue( "Groups", OtherGroups.GetType() ); } catch(Exception){ }
try
{
Dictionary<string, GroupDescription> dict = new();
dict = (Dictionary<string, GroupDescription>) info.GetValue( "FileToGameAndGroups", dict.GetType());
foreach (var pair in dict)
FileToGameAndGroup.Add(pair.Key, pair.Value.ToTuple(this));
} catch (Exception){ }
}
public virtual void GetObjectData(SerializationInfo info, StreamingContext context)
{
if ((TopTypes?.Count ?? 0) > 0) info.AddValue("TopTypes", TopTypes);
if ((BottomTypes?.Count ?? 0) > 0) info.AddValue("BottomTypes", BottomTypes);
if ((OtherGroups?.Count ?? 0) > 0) info.AddValue("Groups", OtherGroups);
if ((FileToGameAndGroup?.Count ?? 0) > 0)
{
var dict = FileToGameAndGroup.ToDictionary( pair => pair.Key, pair => new GroupDescription( this, pair.Value ) );
info.AddValue("FileToGameAndGroups", dict);
}
}
}
}

View file

@ -6,9 +6,12 @@ namespace Penumbra.Models
public class ModInfo
{
public string FolderName { get; set; }
public bool Enabled { get; set; }
public int Priority { get; set; }
public bool Enabled { get; set; }
public int Priority { get; set; }
public int CurrentTop { get; set; } = 0;
public int CurrentBottom { get; set; } = 0;
public int CurrentGroup { get; set; } = 0;
[JsonIgnore]
public ResourceMod Mod { get; set; }
}

View file

@ -10,11 +10,10 @@ namespace Penumbra.Models
public string Version { get; set; }
public string Website { get; set; }
public Dictionary< string, string > FileSwaps { get; } = new();
public string Website { get; set; }
public List<string> ChangedItems { get; set; } = new();
public GroupInformation Groups { get; set; } = new();
}
}

View file

@ -156,6 +156,9 @@ namespace Penumbra.Mods
var entry = new ModInfo
{
Priority = ModSettings.Count,
CurrentGroup = 0,
CurrentTop = 0,
CurrentBottom = 0,
FolderName = mod.ModBasePath.Name,
Enabled = true,
Mod = mod
@ -181,12 +184,23 @@ namespace Penumbra.Mods
return AddModSettings( mod );
}
public IEnumerable< ResourceMod > GetOrderedAndEnabledModList()
public IEnumerable<ModInfo> GetOrderedAndEnabledModSettings()
{
return ModSettings
.Where( x => x.Enabled )
.OrderBy( x => x.Priority )
.OrderBy( x => x.Priority );
}
public IEnumerable<ResourceMod> GetOrderedAndEnabledModList()
{
return GetOrderedAndEnabledModSettings()
.Select( x => x.Mod );
}
public IEnumerable<(ResourceMod, ModInfo)> GetOrderedAndEnabledModListWithSettings()
{
return GetOrderedAndEnabledModSettings()
.Select( x => (x.Mod, x) );
}
}
}

View file

@ -1,15 +1,16 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Windows.Forms.VisualStyles;
using Penumbra.Models;
using Swan.Logging;
namespace Penumbra.Mods
{
public class ModManager : IDisposable
{
private readonly Plugin _plugin;
public readonly Dictionary< string, FileInfo > ResolvedFiles = new();
public readonly Dictionary< string, string > SwappedFiles = new();
public ModCollection Mods { get; set; }
@ -106,44 +107,46 @@ namespace Penumbra.Mods
public void CalculateEffectiveFileList()
{
ResolvedFiles.Clear();
SwappedFiles.Clear();
var registeredFiles = new Dictionary< string, string >();
foreach( var mod in Mods.GetOrderedAndEnabledModList() )
{
foreach( var (mod, settings) in Mods.GetOrderedAndEnabledModListWithSettings() )
{
mod.FileConflicts?.Clear();
// fixup path
var baseDir = mod.ModBasePath.FullName;
foreach( var file in mod.ModFiles )
{
var gamePath = file.FullName.Substring( baseDir.Length )
.TrimStart( '\\' ).Replace( '\\', '/' );
if( !ResolvedFiles.ContainsKey( gamePath ) )
foreach( var file in mod.ModFiles )
{
var relativeFilePath = file.FullName.Substring( baseDir.Length ).TrimStart( '\\' );
string gamePath;
bool addFile = true;
(string, uint, uint, ulong) tuple;
if (mod.Meta.Groups.FileToGameAndGroup.TryGetValue(relativeFilePath, out tuple))
{
gamePath = tuple.Item1;
var (_, tops, bottoms, excludes) = tuple;
var validTop = ((1u << settings.CurrentTop) & tops ) != 0;
var validBottom = ((1u << settings.CurrentBottom) & bottoms ) != 0;
var validGroup = ((1ul << settings.CurrentGroup) & excludes) != 0;
addFile = validTop && validBottom && validGroup;
}
else
gamePath = relativeFilePath.Replace( '\\', '/' );
if ( addFile )
{
ResolvedFiles[ gamePath.ToLowerInvariant() ] = file;
registeredFiles[ gamePath ] = mod.Meta.Name;
}
else if( registeredFiles.TryGetValue( gamePath, out var modName ) )
{
mod.AddConflict( modName, gamePath );
}
}
foreach( var swap in mod.Meta.FileSwaps )
{
// just assume people put not fucked paths in here lol
if( !SwappedFiles.ContainsKey( swap.Value ) )
{
SwappedFiles[ swap.Key.ToLowerInvariant() ] = swap.Value;
registeredFiles[ swap.Key ] = mod.Meta.Name;
}
else if( registeredFiles.TryGetValue( swap.Key, out var modName ) )
{
mod.AddConflict( modName, swap.Key );
if( !ResolvedFiles.ContainsKey( gamePath ) )
{
ResolvedFiles[ gamePath.ToLowerInvariant() ] = file;
registeredFiles[ gamePath ] = mod.Meta.Name;
}
else if( registeredFiles.TryGetValue( gamePath, out var modName ) )
{
mod.AddConflict( modName, gamePath );
}
}
}
}
@ -161,7 +164,6 @@ namespace Penumbra.Mods
DiscoverMods();
}
public FileInfo GetCandidateForGameFile( string gameResourcePath )
{
var val = ResolvedFiles.TryGetValue( gameResourcePath, out var candidate );
@ -178,16 +180,11 @@ namespace Penumbra.Mods
return candidate;
}
public string GetSwappedFilePath( string gameResourcePath )
{
return SwappedFiles.TryGetValue( gameResourcePath, out var swappedPath ) ? swappedPath : null;
}
public string ResolveSwappedOrReplacementFilePath( string gameResourcePath )
public string ResolveReplacementFilePath( string gameResourcePath )
{
gameResourcePath = gameResourcePath.ToLowerInvariant();
return GetCandidateForGameFile( gameResourcePath )?.FullName ?? GetSwappedFilePath( gameResourcePath );
return GetCandidateForGameFile( gameResourcePath )?.FullName;
}
public void Dispose()

View file

@ -31,6 +31,11 @@ namespace Penumbra.Mods
ModFiles.Add( file );
}
}
// Only add if not in a sub-folder, otherwise it was already added.
foreach( var pair in Meta.Groups.FileToGameAndGroup )
if (pair.Key.IndexOfAny(new char[]{'/', '\\'}) < 0)
ModFiles.Add( new FileInfo(Path.Combine(ModBasePath.FullName, pair.Key)) );
}
public void AddConflict( string modName, string path )

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
@ -129,7 +129,7 @@ namespace Penumbra
PluginLog.Log( "[GetResourceHandler] {0}", gameFsPath );
}
var replacementPath = Plugin.ModManager.ResolveSwappedOrReplacementFilePath( gameFsPath );
var replacementPath = Plugin.ModManager.ResolveReplacementFilePath( gameFsPath );
// path must be < 260 because statically defined array length :(
if( replacementPath == null || replacementPath.Length >= 260 )

View file

@ -558,6 +558,39 @@ namespace Penumbra.UI
}
}
private void DrawGroupSelectors()
{
var hasTopTypes = (_selectedMod.Mod.Meta.Groups.TopTypes?.Count ?? 0) > 1;
var hasBottomTypes = (_selectedMod.Mod.Meta.Groups.BottomTypes?.Count ?? 0) > 1;
var hasGroups = (_selectedMod.Mod.Meta.Groups.OtherGroups?.Count ?? 0) > 1;
var numSelectors = (hasTopTypes ? 1 : 0) + (hasBottomTypes ? 1 : 0) + (hasGroups ? 1 : 0);
var selectorWidth = (ImGui.GetWindowWidth()
- (hasTopTypes ? ImGui.CalcTextSize("Top ").X : 0)
- (hasBottomTypes ? ImGui.CalcTextSize("Bottom ").X : 0)
- (hasGroups ? ImGui.CalcTextSize("Group ").X : 0)) / numSelectors;
void DrawSelector(string label, string propertyName, System.Collections.Generic.List<string> list, bool sameLine)
{
var current = (int) _selectedMod.GetType().GetProperty(propertyName).GetValue(_selectedMod);
ImGui.SetNextItemWidth( selectorWidth );
if (sameLine) ImGui.SameLine();
if ( ImGui.Combo(label, ref current, list.ToArray(), list.Count()) )
{
_selectedMod.GetType().GetProperty(propertyName).SetValue(_selectedMod, current);
_plugin.ModManager.Mods.Save();
_plugin.ModManager.CalculateEffectiveFileList();
}
}
if ( hasTopTypes )
DrawSelector("Top", "CurrentTop", _selectedMod.Mod.Meta.Groups.TopTypes, false);
if ( hasBottomTypes )
DrawSelector("Bottom", "CurrentBottom", _selectedMod.Mod.Meta.Groups.BottomTypes, hasTopTypes);
if ( hasGroups )
DrawSelector("Group", "CurrentGroup", _selectedMod.Mod.Meta.Groups.OtherGroups, numSelectors > 1);
}
void DrawInstalledMods()
{
@ -621,6 +654,7 @@ namespace Penumbra.UI
DrawEditButtons();
DrawGroupSelectors();
ImGui.TextWrapped( _selectedMod.Mod.Meta.Description ?? "" );
@ -649,25 +683,6 @@ namespace Penumbra.UI
ImGui.EndTabItem();
}
if( _selectedMod.Mod.Meta.FileSwaps.Any() )
{
if( ImGui.BeginTabItem( "File Swaps" ) )
{
ImGui.SetNextItemWidth( -1 );
if( ImGui.ListBoxHeader( "##", AutoFillSize ) )
{
foreach( var file in _selectedMod.Mod.Meta.FileSwaps )
{
// todo: fucking gross alloc every frame * items
ImGui.Selectable( $"{file.Key} -> {file.Value}" );
}
}
ImGui.ListBoxFooter();
ImGui.EndTabItem();
}
}
if( _selectedMod.Mod.FileConflicts.Any() )
{
if( ImGui.BeginTabItem( "File Conflicts" ) )
@ -742,7 +757,7 @@ namespace Penumbra.UI
// todo: virtualise this
foreach( var file in _plugin.ModManager.ResolvedFiles )
{
ImGui.Selectable( file.Value.FullName );
ImGui.Selectable( file.Value.FullName + " -> " + file.Key );
}
}