From 01215b56974870861a7d3c4deb29aa067af0f060 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 14 Jan 2021 14:48:46 +0100 Subject: [PATCH] Adding support for mod groups/options. Removed SwapFiles. --- Penumbra/Importer/TexToolsImport.cs | 36 +++++-- Penumbra/Models/GroupInformation.cs | 157 ++++++++++++++++++++++++++++ Penumbra/Models/ModInfo.cs | 9 +- Penumbra/Models/ModMeta.cs | 5 +- Penumbra/Mods/ModCollection.cs | 18 +++- Penumbra/Mods/ModManager.cs | 75 +++++++------ Penumbra/Mods/ResourceMod.cs | 5 + Penumbra/ResourceLoader.cs | 4 +- Penumbra/UI/SettingsInterface.cs | 55 ++++++---- 9 files changed, 284 insertions(+), 80 deletions(-) create mode 100644 Penumbra/Models/GroupInformation.cs diff --git a/Penumbra/Importer/TexToolsImport.cs b/Penumbra/Importer/TexToolsImport.cs index de183fab..0173bcec 100644 --- a/Penumbra/Importer/TexToolsImport.cs +++ b/Penumbra/Importer/TexToolsImport.cs @@ -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 ) { diff --git a/Penumbra/Models/GroupInformation.cs b/Penumbra/Models/GroupInformation.cs new file mode 100644 index 00000000..50b6cad4 --- /dev/null +++ b/Penumbra/Models/GroupInformation.cs @@ -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 AddGroupTypes(ulong flags, ulong bound, List groupType) + { + List 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 ownTypes, List 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 TopTypes { get; set; } = null; + public List BottomTypes { get; set; } = null; + public List GroupExclusions { get; set; } = null; + + // Customize (De)-Serialization to ignore nulls. + public GroupDescription(SerializationInfo info, StreamingContext context) + { + List readListOrNull(string name) + { + try + { + var ret = (List) info.GetValue(name, typeof(List)); + 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 TopTypes { get; set; } = new(); + public List BottomTypes { get; set; } = new(); + public List 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 FileToGameAndGroup { get; set; } = new(); + + public GroupInformation(){ } + + public GroupInformation(SerializationInfo info, StreamingContext context) + { + try { TopTypes = (List) info.GetValue( "TopTypes", TopTypes.GetType() ); } catch(Exception){ } + try { BottomTypes = (List) info.GetValue( "BottomTypes", BottomTypes.GetType() ); } catch(Exception){ } + try { OtherGroups = (List) info.GetValue( "Groups", OtherGroups.GetType() ); } catch(Exception){ } + try + { + Dictionary dict = new(); + dict = (Dictionary) 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); + } + } + } +} \ No newline at end of file diff --git a/Penumbra/Models/ModInfo.cs b/Penumbra/Models/ModInfo.cs index cbf86482..8eb01437 100644 --- a/Penumbra/Models/ModInfo.cs +++ b/Penumbra/Models/ModInfo.cs @@ -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; } } diff --git a/Penumbra/Models/ModMeta.cs b/Penumbra/Models/ModMeta.cs index 4be46461..9d1b869d 100644 --- a/Penumbra/Models/ModMeta.cs +++ b/Penumbra/Models/ModMeta.cs @@ -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 ChangedItems { get; set; } = new(); + public GroupInformation Groups { get; set; } = new(); } } \ No newline at end of file diff --git a/Penumbra/Mods/ModCollection.cs b/Penumbra/Mods/ModCollection.cs index cfbfd7dd..e0acad54 100644 --- a/Penumbra/Mods/ModCollection.cs +++ b/Penumbra/Mods/ModCollection.cs @@ -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 GetOrderedAndEnabledModSettings() { return ModSettings .Where( x => x.Enabled ) - .OrderBy( x => x.Priority ) + .OrderBy( x => x.Priority ); + } + + public IEnumerable GetOrderedAndEnabledModList() + { + return GetOrderedAndEnabledModSettings() .Select( x => x.Mod ); + } + + public IEnumerable<(ResourceMod, ModInfo)> GetOrderedAndEnabledModListWithSettings() + { + return GetOrderedAndEnabledModSettings() + .Select( x => (x.Mod, x) ); } } } \ No newline at end of file diff --git a/Penumbra/Mods/ModManager.cs b/Penumbra/Mods/ModManager.cs index 14a2c0d1..3ce17242 100644 --- a/Penumbra/Mods/ModManager.cs +++ b/Penumbra/Mods/ModManager.cs @@ -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() diff --git a/Penumbra/Mods/ResourceMod.cs b/Penumbra/Mods/ResourceMod.cs index a150928e..4800841d 100644 --- a/Penumbra/Mods/ResourceMod.cs +++ b/Penumbra/Mods/ResourceMod.cs @@ -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 ) diff --git a/Penumbra/ResourceLoader.cs b/Penumbra/ResourceLoader.cs index 86cf2b44..37b3a323 100644 --- a/Penumbra/ResourceLoader.cs +++ b/Penumbra/ResourceLoader.cs @@ -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 ) diff --git a/Penumbra/UI/SettingsInterface.cs b/Penumbra/UI/SettingsInterface.cs index a5017d8e..ba0e5c1d 100644 --- a/Penumbra/UI/SettingsInterface.cs +++ b/Penumbra/UI/SettingsInterface.cs @@ -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 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 ); } }