mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 10:17:22 +01:00
Added Deduplication button and the ability to point a hdd file to multiple game paths in groups.
This commit is contained in:
parent
fd2e020eec
commit
06b0fb7e0c
8 changed files with 358 additions and 103 deletions
|
|
@ -237,18 +237,18 @@ namespace Penumbra.Importer
|
||||||
GroupName = group.GroupName,
|
GroupName = group.GroupName,
|
||||||
Options = new List<Option>(),
|
Options = new List<Option>(),
|
||||||
};
|
};
|
||||||
foreach( var opt in group.OptionList )
|
foreach( var opt in group.OptionList )
|
||||||
{
|
{
|
||||||
var optio = new Option
|
var optio = new Option
|
||||||
{
|
{
|
||||||
OptionName = opt.Name,
|
OptionName = opt.Name,
|
||||||
OptionDesc = String.IsNullOrEmpty( opt.Description ) ? "" : opt.Description,
|
OptionDesc = String.IsNullOrEmpty(opt.Description) ? "" : opt.Description,
|
||||||
OptionFiles = new Dictionary<string, string>()
|
OptionFiles = new Dictionary<string, HashSet<string>>()
|
||||||
};
|
};
|
||||||
var optDir = new DirectoryInfo( Path.Combine( groupFolder.FullName, opt.Name ) );
|
var optDir = new DirectoryInfo(Path.Combine( groupFolder.FullName, opt.Name));
|
||||||
foreach( var file in optDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) )
|
foreach ( var file in optDir.EnumerateFiles("*.*", SearchOption.AllDirectories) )
|
||||||
{
|
{
|
||||||
optio.OptionFiles[file.FullName.Substring( baseFolder.FullName.Length ).TrimStart( '\\' )] = file.FullName.Substring( optDir.FullName.Length ).TrimStart( '\\' ).Replace( '\\', '/' );
|
optio.AddFile(file.FullName.Substring(baseFolder.FullName.Length).TrimStart('\\'), file.FullName.Substring(optDir.FullName.Length).TrimStart('\\').Replace('\\','/'));
|
||||||
}
|
}
|
||||||
Inf.Options.Add( optio );
|
Inf.Options.Add( optio );
|
||||||
}
|
}
|
||||||
|
|
@ -333,4 +333,4 @@ namespace Penumbra.Importer
|
||||||
return encoding.GetString( ms.ToArray() );
|
return encoding.GetString( ms.ToArray() );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
162
Penumbra/Models/Deduplicator.cs
Normal file
162
Penumbra/Models/Deduplicator.cs
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Collections;
|
||||||
|
using Dalamud.Plugin;
|
||||||
|
|
||||||
|
namespace Penumbra.Models
|
||||||
|
{
|
||||||
|
public class Deduplicator
|
||||||
|
{
|
||||||
|
private DirectoryInfo baseDir;
|
||||||
|
private int baseDirLength;
|
||||||
|
private ModMeta mod;
|
||||||
|
private SHA256 hasher = null;
|
||||||
|
|
||||||
|
private Dictionary<long, List<FileInfo>> filesBySize;
|
||||||
|
|
||||||
|
private ref SHA256 Sha()
|
||||||
|
{
|
||||||
|
if (hasher == null)
|
||||||
|
hasher = SHA256.Create();
|
||||||
|
return ref hasher;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Deduplicator(DirectoryInfo baseDir, ModMeta mod)
|
||||||
|
{
|
||||||
|
this.baseDir = baseDir;
|
||||||
|
this.baseDirLength = baseDir.FullName.Length;
|
||||||
|
this.mod = mod;
|
||||||
|
filesBySize = new();
|
||||||
|
|
||||||
|
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(){ file };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Run()
|
||||||
|
{
|
||||||
|
foreach (var pair in filesBySize)
|
||||||
|
{
|
||||||
|
if (pair.Value.Count < 2)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (pair.Value.Count == 2)
|
||||||
|
{
|
||||||
|
if (CompareFilesDirectly(pair.Value[0], pair.Value[1]))
|
||||||
|
ReplaceFile(pair.Value[0], pair.Value[1]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var deleted = Enumerable.Repeat(false, pair.Value.Count).ToArray();
|
||||||
|
var hashes = pair.Value.Select( F => ComputeHash(F)).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])
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!CompareHashes(hashes[i], hashes[j]))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
ReplaceFile(pair.Value[i], pair.Value[j]);
|
||||||
|
deleted[j] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ClearEmptySubDirectories(baseDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReplaceFile(FileInfo f1, FileInfo f2)
|
||||||
|
{
|
||||||
|
var relName1 = f1.FullName.Substring(baseDirLength).TrimStart('\\');
|
||||||
|
var relName2 = f2.FullName.Substring(baseDirLength).TrimStart('\\');
|
||||||
|
|
||||||
|
var inOption = false;
|
||||||
|
foreach (var group in mod.Groups.Select( g => g.Value.Options))
|
||||||
|
{
|
||||||
|
foreach (var option in group)
|
||||||
|
{
|
||||||
|
if (option.OptionFiles.TryGetValue(relName2, out var values))
|
||||||
|
{
|
||||||
|
inOption = true;
|
||||||
|
foreach (var value in values)
|
||||||
|
option.AddFile(relName1, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!inOption)
|
||||||
|
{
|
||||||
|
const string duplicates = "Duplicates";
|
||||||
|
if (!mod.Groups.ContainsKey(duplicates))
|
||||||
|
{
|
||||||
|
InstallerInfo info = new()
|
||||||
|
{
|
||||||
|
GroupName = duplicates,
|
||||||
|
SelectionType = SelectType.Single,
|
||||||
|
Options = new()
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
OptionName = "Required",
|
||||||
|
OptionDesc = "",
|
||||||
|
OptionFiles = new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
mod.Groups.Add(duplicates, info);
|
||||||
|
}
|
||||||
|
mod.Groups[duplicates].Options[0].AddFile(relName1, relName2.Replace('\\', '/'));
|
||||||
|
mod.Groups[duplicates].Options[0].AddFile(relName1, relName1.Replace('\\', '/'));
|
||||||
|
}
|
||||||
|
PluginLog.Information($"File {relName1} and {relName2} are identical. Deleting the second.");
|
||||||
|
f2.Delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool CompareFilesDirectly(FileInfo f1, FileInfo f2)
|
||||||
|
{
|
||||||
|
return File.ReadAllBytes(f1.FullName).SequenceEqual(File.ReadAllBytes(f2.FullName));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool CompareHashes(byte[] f1, byte[] f2)
|
||||||
|
{
|
||||||
|
return 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace Penumbra.Models
|
namespace Penumbra.Models
|
||||||
{
|
{
|
||||||
|
|
@ -10,12 +11,23 @@ namespace Penumbra.Models
|
||||||
{
|
{
|
||||||
public string OptionName;
|
public string OptionName;
|
||||||
public string OptionDesc;
|
public string OptionDesc;
|
||||||
public Dictionary<string, string> OptionFiles;
|
|
||||||
|
[JsonProperty(ItemConverterType = typeof(SingleOrArrayConverter<string>))]
|
||||||
|
public Dictionary<string, HashSet<string>> OptionFiles;
|
||||||
|
|
||||||
|
public bool AddFile(string filePath, string gamePath)
|
||||||
|
{
|
||||||
|
if (OptionFiles.TryGetValue(filePath, out var set))
|
||||||
|
return set.Add(gamePath);
|
||||||
|
else
|
||||||
|
OptionFiles[filePath] = new(){ gamePath };
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
public struct InstallerInfo
|
|
||||||
{
|
public struct InstallerInfo {
|
||||||
public string GroupName;
|
public string GroupName;
|
||||||
public SelectType SelectionType;
|
public SelectType SelectionType;
|
||||||
public List<Option> Options;
|
public List<Option> Options;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace Penumbra.Models
|
namespace Penumbra.Models
|
||||||
{
|
{
|
||||||
|
|
@ -17,6 +18,9 @@ namespace Penumbra.Models
|
||||||
|
|
||||||
public Dictionary< string, string > FileSwaps { get; } = new();
|
public Dictionary< string, string > FileSwaps { get; } = new();
|
||||||
|
|
||||||
public Dictionary<string, InstallerInfo> Groups { get; set; } = new();
|
public Dictionary<string, InstallerInfo> Groups { get; set; } = new();
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public bool HasGroupWithConfig { get; set; } = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -69,6 +69,8 @@ namespace Penumbra.Mods
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
meta = JsonConvert.DeserializeObject< ModMeta >( File.ReadAllText( metaFile.FullName ) );
|
meta = JsonConvert.DeserializeObject< ModMeta >( File.ReadAllText( metaFile.FullName ) );
|
||||||
|
meta.HasGroupWithConfig = meta.Groups != null && meta.Groups.Count > 0
|
||||||
|
&& meta.Groups.Values.Any( G => G.SelectionType == SelectType.Multi || G.Options.Count > 1);
|
||||||
}
|
}
|
||||||
catch( Exception e )
|
catch( Exception e )
|
||||||
{
|
{
|
||||||
|
|
@ -218,4 +220,4 @@ namespace Penumbra.Mods
|
||||||
.Select( x => (x.Mod, x) );
|
.Select( x => (x.Mod, x) );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using Penumbra.Models;
|
using Penumbra.Models;
|
||||||
|
|
||||||
namespace Penumbra.Mods
|
namespace Penumbra.Mods
|
||||||
|
|
@ -102,21 +103,7 @@ namespace Penumbra.Mods
|
||||||
// Needed to reload body textures with mods
|
// Needed to reload body textures with mods
|
||||||
//_plugin.GameUtils.ReloadPlayerResources();
|
//_plugin.GameUtils.ReloadPlayerResources();
|
||||||
}
|
}
|
||||||
private (InstallerInfo, Option, string) GlobalPosition( string rel, Dictionary<string, InstallerInfo> gps )
|
|
||||||
{
|
|
||||||
string filePath = null;
|
|
||||||
foreach( var g in gps )
|
|
||||||
{
|
|
||||||
foreach( var opt in g.Value.Options )
|
|
||||||
{
|
|
||||||
if( opt.OptionFiles.TryGetValue( rel, out filePath ) )
|
|
||||||
{
|
|
||||||
return (g.Value, opt, filePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (default( InstallerInfo ), default( Option ), null);
|
|
||||||
}
|
|
||||||
public void CalculateEffectiveFileList()
|
public void CalculateEffectiveFileList()
|
||||||
{
|
{
|
||||||
ResolvedFiles.Clear();
|
ResolvedFiles.Clear();
|
||||||
|
|
@ -131,65 +118,83 @@ namespace Penumbra.Mods
|
||||||
// fixup path
|
// fixup path
|
||||||
var baseDir = mod.ModBasePath.FullName;
|
var baseDir = mod.ModBasePath.FullName;
|
||||||
|
|
||||||
|
if(settings.Conf == null) {
|
||||||
|
settings.Conf = new();
|
||||||
|
_plugin.ModManager.Mods.Save();
|
||||||
|
}
|
||||||
|
|
||||||
foreach( var file in mod.ModFiles )
|
foreach( var file in mod.ModFiles )
|
||||||
{
|
{
|
||||||
var relativeFilePath = file.FullName.Substring( baseDir.Length ).TrimStart( '\\' );
|
var relativeFilePath = file.FullName.Substring( baseDir.Length ).TrimStart( '\\' );
|
||||||
|
|
||||||
|
bool doNotAdd = false;
|
||||||
|
void AddFiles(HashSet<string> gamePaths)
|
||||||
|
{
|
||||||
|
doNotAdd = true;
|
||||||
|
foreach (var gamePath in gamePaths)
|
||||||
|
{
|
||||||
|
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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
string gamePath;
|
HashSet<string> paths;
|
||||||
bool addFile = true;
|
foreach (var group in mod.Meta.Groups.Select( G => G.Value))
|
||||||
var gps = mod.Meta.Groups;
|
{
|
||||||
if( gps.Count >= 1 )
|
if (!settings.Conf.TryGetValue(group.GroupName, out var setting)
|
||||||
{
|
|| (group.SelectionType == SelectType.Single && settings.Conf[group.GroupName] >= group.Options.Count))
|
||||||
var negivtron = GlobalPosition( relativeFilePath, gps );
|
|
||||||
if( negivtron.Item3 != null )
|
|
||||||
{
|
{
|
||||||
if( settings.Conf == null )
|
settings.Conf[group.GroupName] = 0;
|
||||||
{
|
_plugin.ModManager.Mods.Save();
|
||||||
settings.Conf = new();
|
setting = 0;
|
||||||
_plugin.ModManager.Mods.Save();
|
}
|
||||||
}
|
|
||||||
if( !settings.Conf.ContainsKey( negivtron.Item1.GroupName ) )
|
if (group.Options.Count == 0)
|
||||||
{
|
continue;
|
||||||
settings.Conf[negivtron.Item1.GroupName] = 0;
|
|
||||||
_plugin.ModManager.Mods.Save();
|
if (group.SelectionType == SelectType.Multi)
|
||||||
}
|
settings.Conf[group.GroupName] &= ((1 << group.Options.Count) - 1);
|
||||||
var current = settings.Conf[negivtron.Item1.GroupName];
|
|
||||||
var flag = negivtron.Item1.Options.IndexOf( negivtron.Item2 );
|
switch(group.SelectionType)
|
||||||
switch( negivtron.Item1.SelectionType )
|
|
||||||
{
|
|
||||||
case SelectType.Single:
|
|
||||||
{
|
|
||||||
addFile = current == flag;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case SelectType.Multi:
|
|
||||||
{
|
|
||||||
flag = 1 << negivtron.Item1.Options.IndexOf( negivtron.Item2 );
|
|
||||||
addFile = ( flag & current ) != 0;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
gamePath = negivtron.Item3;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
gamePath = relativeFilePath.Replace( '\\', '/' );
|
case SelectType.Single:
|
||||||
}
|
if (group.Options[setting].OptionFiles.TryGetValue(relativeFilePath, out paths))
|
||||||
}
|
AddFiles(paths);
|
||||||
else
|
else
|
||||||
gamePath = relativeFilePath.Replace( '\\', '/' );
|
{
|
||||||
if( addFile )
|
for(var i = 0; i < group.Options.Count; ++i)
|
||||||
{
|
{
|
||||||
if( !ResolvedFiles.ContainsKey( gamePath ) ) {
|
if (i == setting)
|
||||||
ResolvedFiles[ gamePath.ToLowerInvariant() ] = file;
|
continue;
|
||||||
registeredFiles[ gamePath ] = mod.Meta.Name;
|
if(group.Options[i].OptionFiles.ContainsKey(relativeFilePath))
|
||||||
}
|
{
|
||||||
else if( registeredFiles.TryGetValue( gamePath, out var modName ) )
|
doNotAdd = true;
|
||||||
{
|
break;
|
||||||
mod.AddConflict( modName, gamePath );
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case SelectType.Multi:
|
||||||
|
for(var i = 0; i < group.Options.Count; ++i)
|
||||||
|
{
|
||||||
|
if ((setting & (1 << i)) != 0)
|
||||||
|
if (group.Options[i].OptionFiles.TryGetValue(relativeFilePath, out paths))
|
||||||
|
AddFiles(paths);
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!doNotAdd)
|
||||||
|
AddFiles(new() { relativeFilePath.Replace( '\\', '/' ) });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
foreach( var swap in mod.Meta.FileSwaps )
|
foreach( var swap in mod.Meta.FileSwaps )
|
||||||
{
|
{
|
||||||
// just assume people put not fucked paths in here lol
|
// just assume people put not fucked paths in here lol
|
||||||
|
|
@ -253,4 +258,4 @@ namespace Penumbra.Mods
|
||||||
// _fileSystemWatcher?.Dispose();
|
// _fileSystemWatcher?.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.ComponentModel.Design;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
@ -547,11 +548,7 @@ namespace Penumbra.UI
|
||||||
}
|
}
|
||||||
|
|
||||||
if( ImGui.IsItemHovered() )
|
if( ImGui.IsItemHovered() )
|
||||||
{
|
ImGui.SetTooltip( _selectedMod.Mod.Meta.Website );
|
||||||
ImGui.BeginTooltip();
|
|
||||||
ImGui.Text( _selectedMod.Mod.Meta.Website );
|
|
||||||
ImGui.EndTooltip();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -568,7 +565,9 @@ namespace Penumbra.UI
|
||||||
if( ImGui.Button( "Open Mod Folder" ) )
|
if( ImGui.Button( "Open Mod Folder" ) )
|
||||||
{
|
{
|
||||||
Process.Start( _selectedMod.Mod.ModBasePath.FullName );
|
Process.Start( _selectedMod.Mod.ModBasePath.FullName );
|
||||||
}
|
}
|
||||||
|
if( ImGui.IsItemHovered() )
|
||||||
|
ImGui.SetTooltip( "Open the directory containing this mod in your default file explorer." );
|
||||||
|
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
if( ImGui.Button( "Edit JSON" ) )
|
if( ImGui.Button( "Edit JSON" ) )
|
||||||
|
|
@ -576,24 +575,28 @@ namespace Penumbra.UI
|
||||||
var metaPath = Path.Combine( _selectedMod.Mod.ModBasePath.FullName, "meta.json" );
|
var metaPath = Path.Combine( _selectedMod.Mod.ModBasePath.FullName, "meta.json" );
|
||||||
File.WriteAllText( metaPath, JsonConvert.SerializeObject( _selectedMod.Mod.Meta, Formatting.Indented ) );
|
File.WriteAllText( metaPath, JsonConvert.SerializeObject( _selectedMod.Mod.Meta, Formatting.Indented ) );
|
||||||
Process.Start( metaPath );
|
Process.Start( metaPath );
|
||||||
}
|
}
|
||||||
|
if( ImGui.IsItemHovered() )
|
||||||
|
ImGui.SetTooltip( "Open the JSON configuration file in your default application for .json." );
|
||||||
|
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
if( ImGui.Button( "Reload JSON" ) )
|
if( ImGui.Button( "Reload JSON" ) )
|
||||||
{
|
{
|
||||||
ReloadMods();
|
ReloadMods();
|
||||||
|
}
|
||||||
// May select a different mod than before if mods were added or deleted, but will not crash.
|
if( ImGui.IsItemHovered() )
|
||||||
if( _selectedModIndex < _plugin.ModManager.Mods.ModSettings.Count )
|
ImGui.SetTooltip( "Reload the configuration of all mods." );
|
||||||
{
|
|
||||||
_selectedMod = _plugin.ModManager.Mods.ModSettings[ _selectedModIndex ];
|
ImGui.SameLine();
|
||||||
}
|
if( ImGui.Button( "Deduplicate" ) )
|
||||||
else
|
{
|
||||||
{
|
new Deduplicator(_selectedMod.Mod.ModBasePath, _selectedMod.Mod.Meta).Run();
|
||||||
_selectedModIndex = 0;
|
var metaPath = Path.Combine( _selectedMod.Mod.ModBasePath.FullName, "meta.json" );
|
||||||
_selectedMod = null;
|
File.WriteAllText( metaPath, JsonConvert.SerializeObject( _selectedMod.Mod.Meta, Formatting.Indented ) );
|
||||||
}
|
ReloadMods();
|
||||||
}
|
}
|
||||||
|
if( ImGui.IsItemHovered() )
|
||||||
|
ImGui.SetTooltip( "Try to find identical files and remove duplicate occurences to reduce the mods disk size." );
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawGroupSelectors()
|
private void DrawGroupSelectors()
|
||||||
|
|
@ -724,11 +727,12 @@ namespace Penumbra.UI
|
||||||
ImGui.EndTabItem();
|
ImGui.EndTabItem();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(_selectedMod.Mod.Meta.Groups.Count >=1) {
|
if(_selectedMod.Mod.Meta.HasGroupWithConfig) {
|
||||||
if(ImGui.BeginTabItem( "Configuration" )) {
|
if(ImGui.BeginTabItem( "Configuration" ))
|
||||||
|
{
|
||||||
DrawGroupSelectors();
|
DrawGroupSelectors();
|
||||||
ImGui.EndTabItem();
|
ImGui.EndTabItem();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if( ImGui.BeginTabItem( "Files" ) )
|
if( ImGui.BeginTabItem( "Files" ) )
|
||||||
{
|
{
|
||||||
|
|
@ -850,7 +854,18 @@ namespace Penumbra.UI
|
||||||
// create the directory if it doesn't exist
|
// create the directory if it doesn't exist
|
||||||
Directory.CreateDirectory( _plugin.Configuration.CurrentCollection );
|
Directory.CreateDirectory( _plugin.Configuration.CurrentCollection );
|
||||||
|
|
||||||
_plugin.ModManager.DiscoverMods( _plugin.Configuration.CurrentCollection );
|
_plugin.ModManager.DiscoverMods( _plugin.Configuration.CurrentCollection );
|
||||||
|
|
||||||
|
// May select a different mod than before if mods were added or deleted, but will not crash.
|
||||||
|
if( _selectedModIndex < _plugin.ModManager.Mods.ModSettings.Count )
|
||||||
|
{
|
||||||
|
_selectedMod = _plugin.ModManager.Mods.ModSettings[ _selectedModIndex ];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_selectedModIndex = 0;
|
||||||
|
_selectedMod = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
55
Penumbra/Util/SingleOrArrayConverter.cs
Normal file
55
Penumbra/Util/SingleOrArrayConverter.cs
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
public class SingleOrArrayConverter<T> : JsonConverter
|
||||||
|
{
|
||||||
|
public override bool CanConvert( Type objectType )
|
||||||
|
{
|
||||||
|
return (objectType == typeof(HashSet<T>));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override object ReadJson( JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer )
|
||||||
|
{
|
||||||
|
var token = JToken.Load(reader);
|
||||||
|
if (token.Type == JTokenType.Array)
|
||||||
|
{
|
||||||
|
return token.ToObject<HashSet<T>>();
|
||||||
|
}
|
||||||
|
return new HashSet<T>{ token.ToObject<T>() };
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool CanWrite => false;
|
||||||
|
|
||||||
|
public override void WriteJson( JsonWriter writer, object value, JsonSerializer serializer )
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DictSingleOrArrayConverter<T,U> : JsonConverter
|
||||||
|
{
|
||||||
|
public override bool CanConvert( Type objectType )
|
||||||
|
{
|
||||||
|
return (objectType == typeof(Dictionary<T, HashSet<U>>));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override object ReadJson( JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer )
|
||||||
|
{
|
||||||
|
var token = JToken.Load(reader);
|
||||||
|
|
||||||
|
if (token.Type == JTokenType.Array)
|
||||||
|
{
|
||||||
|
return token.ToObject<HashSet<T>>();
|
||||||
|
}
|
||||||
|
return new HashSet<T>{ token.ToObject<T>() };
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool CanWrite => false;
|
||||||
|
|
||||||
|
public override void WriteJson( JsonWriter writer, object value, JsonSerializer serializer )
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue