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(); newModFolder.Create();
File.WriteAllText(
Path.Combine( newModFolder.FullName, "meta.json" ),
JsonConvert.SerializeObject( modMeta )
);
if( modList.SimpleModsList != null ) if( modList.SimpleModsList != null )
ExtractSimpleModList( newModFolder, modList.SimpleModsList, modData ); ExtractSimpleModList( newModFolder, modList.SimpleModsList, modData );
@ -216,15 +211,34 @@ namespace Penumbra.Importer
return; return;
// Iterate through all pages // 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 foreach( var option in from modPackPage in modList.ModPackPages
from modGroup in modPackPage.ModGroups from modGroup in modPackPage.ModGroups
from option in modGroup.OptionList from option in modGroup.OptionList
where option.IsChecked
select option ) select option )
{ {
ExtractSimpleModList( newModFolder, option.ModsJsons, modData ); 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() )
{
foreach( var file in dir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) )
{
meta.Groups.AddFileToOtherGroups(optionName
, file.FullName.Substring(baseFolderLength).TrimStart('\\')
, file.FullName.Substring(optionFolderLength).TrimStart('\\').Replace('\\', '/'));
}
} }
} }

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,8 +6,11 @@ namespace Penumbra.Models
public class ModInfo public class ModInfo
{ {
public string FolderName { get; set; } public string FolderName { get; set; }
public bool Enabled { get; set; } public bool Enabled { get; set; }
public int Priority { 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] [JsonIgnore]
public ResourceMod Mod { get; set; } public ResourceMod Mod { get; set; }

View file

@ -12,9 +12,8 @@ namespace Penumbra.Models
public string Website { get; set; } public string Website { get; set; }
public Dictionary< string, string > FileSwaps { get; } = new();
public List<string> ChangedItems { get; set; } = new(); 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 var entry = new ModInfo
{ {
Priority = ModSettings.Count, Priority = ModSettings.Count,
CurrentGroup = 0,
CurrentTop = 0,
CurrentBottom = 0,
FolderName = mod.ModBasePath.Name, FolderName = mod.ModBasePath.Name,
Enabled = true, Enabled = true,
Mod = mod Mod = mod
@ -181,12 +184,23 @@ namespace Penumbra.Mods
return AddModSettings( mod ); return AddModSettings( mod );
} }
public IEnumerable< ResourceMod > GetOrderedAndEnabledModList() public IEnumerable<ModInfo> GetOrderedAndEnabledModSettings()
{ {
return ModSettings return ModSettings
.Where( x => x.Enabled ) .Where( x => x.Enabled )
.OrderBy( x => x.Priority ) .OrderBy( x => x.Priority );
}
public IEnumerable<ResourceMod> GetOrderedAndEnabledModList()
{
return GetOrderedAndEnabledModSettings()
.Select( x => x.Mod ); .Select( x => x.Mod );
} }
public IEnumerable<(ResourceMod, ModInfo)> GetOrderedAndEnabledModListWithSettings()
{
return GetOrderedAndEnabledModSettings()
.Select( x => (x.Mod, x) );
}
} }
} }

View file

@ -1,7 +1,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Windows.Forms.VisualStyles;
using Penumbra.Models; using Penumbra.Models;
using Swan.Logging;
namespace Penumbra.Mods namespace Penumbra.Mods
{ {
@ -9,7 +11,6 @@ namespace Penumbra.Mods
{ {
private readonly Plugin _plugin; private readonly Plugin _plugin;
public readonly Dictionary< string, FileInfo > ResolvedFiles = new(); public readonly Dictionary< string, FileInfo > ResolvedFiles = new();
public readonly Dictionary< string, string > SwappedFiles = new();
public ModCollection Mods { get; set; } public ModCollection Mods { get; set; }
@ -106,11 +107,10 @@ namespace Penumbra.Mods
public void CalculateEffectiveFileList() public void CalculateEffectiveFileList()
{ {
ResolvedFiles.Clear(); ResolvedFiles.Clear();
SwappedFiles.Clear();
var registeredFiles = new Dictionary< string, string >(); var registeredFiles = new Dictionary< string, string >();
foreach( var mod in Mods.GetOrderedAndEnabledModList() ) foreach( var (mod, settings) in Mods.GetOrderedAndEnabledModListWithSettings() )
{ {
mod.FileConflicts?.Clear(); mod.FileConflicts?.Clear();
@ -119,31 +119,34 @@ namespace Penumbra.Mods
foreach( var file in mod.ModFiles ) foreach( var file in mod.ModFiles )
{ {
var gamePath = file.FullName.Substring( baseDir.Length ) var relativeFilePath = file.FullName.Substring( baseDir.Length ).TrimStart( '\\' );
.TrimStart( '\\' ).Replace( '\\', '/' );
if( !ResolvedFiles.ContainsKey( gamePath ) ) string gamePath;
bool addFile = true;
(string, uint, uint, ulong) tuple;
if (mod.Meta.Groups.FileToGameAndGroup.TryGetValue(relativeFilePath, out tuple))
{ {
ResolvedFiles[ gamePath.ToLowerInvariant() ] = file; gamePath = tuple.Item1;
registeredFiles[ gamePath ] = mod.Meta.Name; 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 if( registeredFiles.TryGetValue( gamePath, out var modName ) ) else
{ gamePath = relativeFilePath.Replace( '\\', '/' );
mod.AddConflict( modName, gamePath );
}
}
foreach( var swap in mod.Meta.FileSwaps ) if ( addFile )
{
// just assume people put not fucked paths in here lol
if( !SwappedFiles.ContainsKey( swap.Value ) )
{ {
SwappedFiles[ swap.Key.ToLowerInvariant() ] = swap.Value; if( !ResolvedFiles.ContainsKey( gamePath ) )
registeredFiles[ swap.Key ] = mod.Meta.Name; {
} ResolvedFiles[ gamePath.ToLowerInvariant() ] = file;
else if( registeredFiles.TryGetValue( swap.Key, out var modName ) ) registeredFiles[ gamePath ] = mod.Meta.Name;
{ }
mod.AddConflict( modName, swap.Key ); else if( registeredFiles.TryGetValue( gamePath, out var modName ) )
{
mod.AddConflict( modName, gamePath );
}
} }
} }
} }
@ -161,7 +164,6 @@ namespace Penumbra.Mods
DiscoverMods(); DiscoverMods();
} }
public FileInfo GetCandidateForGameFile( string gameResourcePath ) public FileInfo GetCandidateForGameFile( string gameResourcePath )
{ {
var val = ResolvedFiles.TryGetValue( gameResourcePath, out var candidate ); var val = ResolvedFiles.TryGetValue( gameResourcePath, out var candidate );
@ -178,16 +180,11 @@ namespace Penumbra.Mods
return candidate; return candidate;
} }
public string GetSwappedFilePath( string gameResourcePath ) public string ResolveReplacementFilePath( string gameResourcePath )
{
return SwappedFiles.TryGetValue( gameResourcePath, out var swappedPath ) ? swappedPath : null;
}
public string ResolveSwappedOrReplacementFilePath( string gameResourcePath )
{ {
gameResourcePath = gameResourcePath.ToLowerInvariant(); gameResourcePath = gameResourcePath.ToLowerInvariant();
return GetCandidateForGameFile( gameResourcePath )?.FullName ?? GetSwappedFilePath( gameResourcePath ); return GetCandidateForGameFile( gameResourcePath )?.FullName;
} }
public void Dispose() public void Dispose()

View file

@ -31,6 +31,11 @@ namespace Penumbra.Mods
ModFiles.Add( file ); 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 ) public void AddConflict( string modName, string path )

View file

@ -1,4 +1,4 @@
using System; using System;
using System.IO; using System.IO;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
@ -129,7 +129,7 @@ namespace Penumbra
PluginLog.Log( "[GetResourceHandler] {0}", gameFsPath ); 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 :( // path must be < 260 because statically defined array length :(
if( replacementPath == null || replacementPath.Length >= 260 ) 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() void DrawInstalledMods()
{ {
@ -621,6 +654,7 @@ namespace Penumbra.UI
DrawEditButtons(); DrawEditButtons();
DrawGroupSelectors();
ImGui.TextWrapped( _selectedMod.Mod.Meta.Description ?? "" ); ImGui.TextWrapped( _selectedMod.Mod.Meta.Description ?? "" );
@ -649,25 +683,6 @@ namespace Penumbra.UI
ImGui.EndTabItem(); 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( _selectedMod.Mod.FileConflicts.Any() )
{ {
if( ImGui.BeginTabItem( "File Conflicts" ) ) if( ImGui.BeginTabItem( "File Conflicts" ) )
@ -742,7 +757,7 @@ namespace Penumbra.UI
// todo: virtualise this // todo: virtualise this
foreach( var file in _plugin.ModManager.ResolvedFiles ) foreach( var file in _plugin.ModManager.ResolvedFiles )
{ {
ImGui.Selectable( file.Value.FullName ); ImGui.Selectable( file.Value.FullName + " -> " + file.Key );
} }
} }