mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +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,
|
||||
Options = new List<Option>(),
|
||||
};
|
||||
foreach( var opt in group.OptionList )
|
||||
foreach( var opt in group.OptionList )
|
||||
{
|
||||
var optio = new Option
|
||||
{
|
||||
OptionName = opt.Name,
|
||||
OptionDesc = String.IsNullOrEmpty( opt.Description ) ? "" : opt.Description,
|
||||
OptionFiles = new Dictionary<string, string>()
|
||||
OptionDesc = String.IsNullOrEmpty(opt.Description) ? "" : opt.Description,
|
||||
OptionFiles = new Dictionary<string, HashSet<string>>()
|
||||
};
|
||||
var optDir = new DirectoryInfo( Path.Combine( groupFolder.FullName, opt.Name ) );
|
||||
foreach( var file in optDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) )
|
||||
var optDir = new DirectoryInfo(Path.Combine( groupFolder.FullName, opt.Name));
|
||||
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 );
|
||||
}
|
||||
|
|
@ -333,4 +333,4 @@ namespace Penumbra.Importer
|
|||
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 Newtonsoft.Json;
|
||||
|
||||
namespace Penumbra.Models
|
||||
{
|
||||
|
|
@ -10,12 +11,23 @@ namespace Penumbra.Models
|
|||
{
|
||||
public string OptionName;
|
||||
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 SelectType SelectionType;
|
||||
public List<Option> Options;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Penumbra.Models
|
||||
{
|
||||
|
|
@ -17,6 +18,9 @@ namespace Penumbra.Models
|
|||
|
||||
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
|
||||
{
|
||||
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 )
|
||||
{
|
||||
|
|
@ -218,4 +220,4 @@ namespace Penumbra.Mods
|
|||
.Select( x => (x.Mod, x) );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Penumbra.Models;
|
||||
|
||||
namespace Penumbra.Mods
|
||||
|
|
@ -102,21 +103,7 @@ namespace Penumbra.Mods
|
|||
// Needed to reload body textures with mods
|
||||
//_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()
|
||||
{
|
||||
ResolvedFiles.Clear();
|
||||
|
|
@ -131,65 +118,83 @@ namespace Penumbra.Mods
|
|||
// fixup path
|
||||
var baseDir = mod.ModBasePath.FullName;
|
||||
|
||||
if(settings.Conf == null) {
|
||||
settings.Conf = new();
|
||||
_plugin.ModManager.Mods.Save();
|
||||
}
|
||||
|
||||
foreach( var file in mod.ModFiles )
|
||||
{
|
||||
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;
|
||||
bool addFile = true;
|
||||
var gps = mod.Meta.Groups;
|
||||
if( gps.Count >= 1 )
|
||||
{
|
||||
var negivtron = GlobalPosition( relativeFilePath, gps );
|
||||
if( negivtron.Item3 != null )
|
||||
HashSet<string> paths;
|
||||
foreach (var group in mod.Meta.Groups.Select( G => G.Value))
|
||||
{
|
||||
if (!settings.Conf.TryGetValue(group.GroupName, out var setting)
|
||||
|| (group.SelectionType == SelectType.Single && settings.Conf[group.GroupName] >= group.Options.Count))
|
||||
{
|
||||
if( settings.Conf == null )
|
||||
{
|
||||
settings.Conf = new();
|
||||
_plugin.ModManager.Mods.Save();
|
||||
}
|
||||
if( !settings.Conf.ContainsKey( negivtron.Item1.GroupName ) )
|
||||
{
|
||||
settings.Conf[negivtron.Item1.GroupName] = 0;
|
||||
_plugin.ModManager.Mods.Save();
|
||||
}
|
||||
var current = settings.Conf[negivtron.Item1.GroupName];
|
||||
var flag = negivtron.Item1.Options.IndexOf( negivtron.Item2 );
|
||||
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
|
||||
settings.Conf[group.GroupName] = 0;
|
||||
_plugin.ModManager.Mods.Save();
|
||||
setting = 0;
|
||||
}
|
||||
|
||||
if (group.Options.Count == 0)
|
||||
continue;
|
||||
|
||||
if (group.SelectionType == SelectType.Multi)
|
||||
settings.Conf[group.GroupName] &= ((1 << group.Options.Count) - 1);
|
||||
|
||||
switch(group.SelectionType)
|
||||
{
|
||||
gamePath = relativeFilePath.Replace( '\\', '/' );
|
||||
}
|
||||
}
|
||||
else
|
||||
gamePath = relativeFilePath.Replace( '\\', '/' );
|
||||
if( addFile )
|
||||
{
|
||||
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 );
|
||||
case SelectType.Single:
|
||||
if (group.Options[setting].OptionFiles.TryGetValue(relativeFilePath, out paths))
|
||||
AddFiles(paths);
|
||||
else
|
||||
{
|
||||
for(var i = 0; i < group.Options.Count; ++i)
|
||||
{
|
||||
if (i == setting)
|
||||
continue;
|
||||
if(group.Options[i].OptionFiles.ContainsKey(relativeFilePath))
|
||||
{
|
||||
doNotAdd = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
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 )
|
||||
{
|
||||
// just assume people put not fucked paths in here lol
|
||||
|
|
@ -253,4 +258,4 @@ namespace Penumbra.Mods
|
|||
// _fileSystemWatcher?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.ComponentModel.Design;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
|
@ -547,11 +548,7 @@ namespace Penumbra.UI
|
|||
}
|
||||
|
||||
if( ImGui.IsItemHovered() )
|
||||
{
|
||||
ImGui.BeginTooltip();
|
||||
ImGui.Text( _selectedMod.Mod.Meta.Website );
|
||||
ImGui.EndTooltip();
|
||||
}
|
||||
ImGui.SetTooltip( _selectedMod.Mod.Meta.Website );
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -568,7 +565,9 @@ namespace Penumbra.UI
|
|||
if( ImGui.Button( "Open Mod Folder" ) )
|
||||
{
|
||||
Process.Start( _selectedMod.Mod.ModBasePath.FullName );
|
||||
}
|
||||
}
|
||||
if( ImGui.IsItemHovered() )
|
||||
ImGui.SetTooltip( "Open the directory containing this mod in your default file explorer." );
|
||||
|
||||
ImGui.SameLine();
|
||||
if( ImGui.Button( "Edit JSON" ) )
|
||||
|
|
@ -576,24 +575,28 @@ namespace Penumbra.UI
|
|||
var metaPath = Path.Combine( _selectedMod.Mod.ModBasePath.FullName, "meta.json" );
|
||||
File.WriteAllText( metaPath, JsonConvert.SerializeObject( _selectedMod.Mod.Meta, Formatting.Indented ) );
|
||||
Process.Start( metaPath );
|
||||
}
|
||||
}
|
||||
if( ImGui.IsItemHovered() )
|
||||
ImGui.SetTooltip( "Open the JSON configuration file in your default application for .json." );
|
||||
|
||||
ImGui.SameLine();
|
||||
if( ImGui.Button( "Reload JSON" ) )
|
||||
{
|
||||
ReloadMods();
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
if( ImGui.IsItemHovered() )
|
||||
ImGui.SetTooltip( "Reload the configuration of all mods." );
|
||||
|
||||
ImGui.SameLine();
|
||||
if( ImGui.Button( "Deduplicate" ) )
|
||||
{
|
||||
new Deduplicator(_selectedMod.Mod.ModBasePath, _selectedMod.Mod.Meta).Run();
|
||||
var metaPath = Path.Combine( _selectedMod.Mod.ModBasePath.FullName, "meta.json" );
|
||||
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()
|
||||
|
|
@ -724,11 +727,12 @@ namespace Penumbra.UI
|
|||
ImGui.EndTabItem();
|
||||
}
|
||||
}
|
||||
if(_selectedMod.Mod.Meta.Groups.Count >=1) {
|
||||
if(ImGui.BeginTabItem( "Configuration" )) {
|
||||
if(_selectedMod.Mod.Meta.HasGroupWithConfig) {
|
||||
if(ImGui.BeginTabItem( "Configuration" ))
|
||||
{
|
||||
DrawGroupSelectors();
|
||||
ImGui.EndTabItem();
|
||||
}
|
||||
}
|
||||
}
|
||||
if( ImGui.BeginTabItem( "Files" ) )
|
||||
{
|
||||
|
|
@ -850,7 +854,18 @@ namespace Penumbra.UI
|
|||
// create the directory if it doesn't exist
|
||||
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