mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
Change most things to new byte strings, introduce new ResourceLoader and Logger fully.
This commit is contained in:
parent
5d77cd5514
commit
f5fccb0235
55 changed files with 2681 additions and 2730 deletions
|
|
@ -1,17 +1,21 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Penumbra.GameData.Util;
|
||||
|
||||
namespace Penumbra.GameData.ByteString;
|
||||
|
||||
[JsonConverter( typeof( FullPathConverter ) )]
|
||||
public readonly struct FullPath : IComparable, IEquatable< FullPath >
|
||||
{
|
||||
public readonly string FullName;
|
||||
public readonly Utf8String InternalName;
|
||||
public readonly ulong Crc64;
|
||||
|
||||
public static readonly FullPath Empty = new(string.Empty);
|
||||
|
||||
public FullPath( DirectoryInfo baseDir, NewRelPath relPath )
|
||||
public FullPath( DirectoryInfo baseDir, Utf8RelPath relPath )
|
||||
: this( Path.Combine( baseDir.FullName, relPath.ToString() ) )
|
||||
{ }
|
||||
|
||||
|
|
@ -19,10 +23,11 @@ public readonly struct FullPath : IComparable, IEquatable< FullPath >
|
|||
: this( file.FullName )
|
||||
{ }
|
||||
|
||||
|
||||
public FullPath( string s )
|
||||
{
|
||||
FullName = s;
|
||||
InternalName = Utf8String.FromString( FullName, out var name, true ) ? name : Utf8String.Empty;
|
||||
InternalName = Utf8String.FromString( FullName, out var name, true ) ? name.Replace( ( byte )'\\', ( byte )'/' ) : Utf8String.Empty;
|
||||
Crc64 = Functions.ComputeCrc64( InternalName.Span );
|
||||
}
|
||||
|
||||
|
|
@ -35,9 +40,9 @@ public readonly struct FullPath : IComparable, IEquatable< FullPath >
|
|||
public string Name
|
||||
=> Path.GetFileName( FullName );
|
||||
|
||||
public bool ToGamePath( DirectoryInfo dir, out NewGamePath path )
|
||||
public bool ToGamePath( DirectoryInfo dir, out Utf8GamePath path )
|
||||
{
|
||||
path = NewGamePath.Empty;
|
||||
path = Utf8GamePath.Empty;
|
||||
if( !InternalName.IsAscii || !FullName.StartsWith( dir.FullName ) )
|
||||
{
|
||||
return false;
|
||||
|
|
@ -45,13 +50,13 @@ public readonly struct FullPath : IComparable, IEquatable< FullPath >
|
|||
|
||||
var substring = InternalName.Substring( dir.FullName.Length + 1 );
|
||||
|
||||
path = new NewGamePath( substring.Replace( ( byte )'\\', ( byte )'/' ) );
|
||||
path = new Utf8GamePath( substring );
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool ToRelPath( DirectoryInfo dir, out NewRelPath path )
|
||||
public bool ToRelPath( DirectoryInfo dir, out Utf8RelPath path )
|
||||
{
|
||||
path = NewRelPath.Empty;
|
||||
path = Utf8RelPath.Empty;
|
||||
if( !FullName.StartsWith( dir.FullName ) )
|
||||
{
|
||||
return false;
|
||||
|
|
@ -59,7 +64,7 @@ public readonly struct FullPath : IComparable, IEquatable< FullPath >
|
|||
|
||||
var substring = InternalName.Substring( dir.FullName.Length + 1 );
|
||||
|
||||
path = new NewRelPath( substring );
|
||||
path = new Utf8RelPath( substring.Replace( ( byte )'/', ( byte )'\\' ) );
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -88,9 +93,35 @@ public readonly struct FullPath : IComparable, IEquatable< FullPath >
|
|||
return InternalName.Equals( other.InternalName );
|
||||
}
|
||||
|
||||
public bool IsRooted
|
||||
=> new Utf8GamePath( InternalName ).IsRooted();
|
||||
|
||||
public override int GetHashCode()
|
||||
=> InternalName.Crc32;
|
||||
|
||||
public override string ToString()
|
||||
=> FullName;
|
||||
|
||||
public class FullPathConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert( Type objectType )
|
||||
=> objectType == typeof( FullPath );
|
||||
|
||||
public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer )
|
||||
{
|
||||
var token = JToken.Load( reader ).ToString();
|
||||
return new FullPath( token );
|
||||
}
|
||||
|
||||
public override bool CanWrite
|
||||
=> true;
|
||||
|
||||
public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer )
|
||||
{
|
||||
if( value is FullPath p )
|
||||
{
|
||||
serializer.Serialize( writer, p.ToString() );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,20 +3,21 @@ using System.IO;
|
|||
using Dalamud.Utility;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Penumbra.GameData.Util;
|
||||
|
||||
namespace Penumbra.GameData.ByteString;
|
||||
|
||||
// NewGamePath wrap some additional validity checking around Utf8String,
|
||||
// provide some filesystem helpers, and conversion to Json.
|
||||
[JsonConverter( typeof( NewGamePathConverter ) )]
|
||||
public readonly struct NewGamePath : IEquatable< NewGamePath >, IComparable< NewGamePath >, IDisposable
|
||||
[JsonConverter( typeof( Utf8GamePathConverter ) )]
|
||||
public readonly struct Utf8GamePath : IEquatable< Utf8GamePath >, IComparable< Utf8GamePath >, IDisposable
|
||||
{
|
||||
public const int MaxGamePathLength = 256;
|
||||
|
||||
public readonly Utf8String Path;
|
||||
public static readonly NewGamePath Empty = new(Utf8String.Empty);
|
||||
public readonly Utf8String Path;
|
||||
public static readonly Utf8GamePath Empty = new(Utf8String.Empty);
|
||||
|
||||
internal NewGamePath( Utf8String s )
|
||||
internal Utf8GamePath( Utf8String s )
|
||||
=> Path = s;
|
||||
|
||||
public int Length
|
||||
|
|
@ -25,16 +26,16 @@ public readonly struct NewGamePath : IEquatable< NewGamePath >, IComparable< New
|
|||
public bool IsEmpty
|
||||
=> Path.IsEmpty;
|
||||
|
||||
public NewGamePath ToLower()
|
||||
public Utf8GamePath ToLower()
|
||||
=> new(Path.AsciiToLower());
|
||||
|
||||
public static unsafe bool FromPointer( byte* ptr, out NewGamePath path, bool lower = false )
|
||||
public static unsafe bool FromPointer( byte* ptr, out Utf8GamePath path, bool lower = false )
|
||||
{
|
||||
var utf = new Utf8String( ptr );
|
||||
return ReturnChecked( utf, out path, lower );
|
||||
}
|
||||
|
||||
public static bool FromSpan( ReadOnlySpan< byte > data, out NewGamePath path, bool lower = false )
|
||||
public static bool FromSpan( ReadOnlySpan< byte > data, out Utf8GamePath path, bool lower = false )
|
||||
{
|
||||
var utf = Utf8String.FromSpanUnsafe( data, false, null, null );
|
||||
return ReturnChecked( utf, out path, lower );
|
||||
|
|
@ -43,7 +44,7 @@ public readonly struct NewGamePath : IEquatable< NewGamePath >, IComparable< New
|
|||
// Does not check for Forward/Backslashes due to assuming that SE-strings use the correct one.
|
||||
// Does not check for initial slashes either, since they are assumed to be by choice.
|
||||
// Checks for maxlength, ASCII and lowercase.
|
||||
private static bool ReturnChecked( Utf8String utf, out NewGamePath path, bool lower = false )
|
||||
private static bool ReturnChecked( Utf8String utf, out Utf8GamePath path, bool lower = false )
|
||||
{
|
||||
path = Empty;
|
||||
if( !utf.IsAscii || utf.Length > MaxGamePathLength )
|
||||
|
|
@ -51,14 +52,17 @@ public readonly struct NewGamePath : IEquatable< NewGamePath >, IComparable< New
|
|||
return false;
|
||||
}
|
||||
|
||||
path = new NewGamePath( lower ? utf.AsciiToLower() : utf );
|
||||
path = new Utf8GamePath( lower ? utf.AsciiToLower() : utf );
|
||||
return true;
|
||||
}
|
||||
|
||||
public NewGamePath Clone()
|
||||
public Utf8GamePath Clone()
|
||||
=> new(Path.Clone());
|
||||
|
||||
public static bool FromString( string? s, out NewGamePath path, bool toLower = false )
|
||||
public static explicit operator Utf8GamePath( string s )
|
||||
=> FromString( s, out var p, true ) ? p : Empty;
|
||||
|
||||
public static bool FromString( string? s, out Utf8GamePath path, bool toLower = false )
|
||||
{
|
||||
path = Empty;
|
||||
if( s.IsNullOrEmpty() )
|
||||
|
|
@ -83,11 +87,11 @@ public readonly struct NewGamePath : IEquatable< NewGamePath >, IComparable< New
|
|||
return false;
|
||||
}
|
||||
|
||||
path = new NewGamePath( ascii );
|
||||
path = new Utf8GamePath( ascii );
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool FromFile( FileInfo file, DirectoryInfo baseDir, out NewGamePath path, bool toLower = false )
|
||||
public static bool FromFile( FileInfo file, DirectoryInfo baseDir, out Utf8GamePath path, bool toLower = false )
|
||||
{
|
||||
path = Empty;
|
||||
if( !file.FullName.StartsWith( baseDir.FullName ) )
|
||||
|
|
@ -111,13 +115,13 @@ public readonly struct NewGamePath : IEquatable< NewGamePath >, IComparable< New
|
|||
return idx == -1 ? Utf8String.Empty : Path.Substring( idx );
|
||||
}
|
||||
|
||||
public bool Equals( NewGamePath other )
|
||||
public bool Equals( Utf8GamePath other )
|
||||
=> Path.Equals( other.Path );
|
||||
|
||||
public override int GetHashCode()
|
||||
=> Path.GetHashCode();
|
||||
|
||||
public int CompareTo( NewGamePath other )
|
||||
public int CompareTo( Utf8GamePath other )
|
||||
=> Path.CompareTo( other.Path );
|
||||
|
||||
public override string ToString()
|
||||
|
|
@ -132,17 +136,17 @@ public readonly struct NewGamePath : IEquatable< NewGamePath >, IComparable< New
|
|||
&& ( Path[ 0 ] >= 'A' && Path[ 0 ] <= 'Z' || Path[ 0 ] >= 'a' && Path[ 0 ] <= 'z' )
|
||||
&& Path[ 1 ] == ':';
|
||||
|
||||
private class NewGamePathConverter : JsonConverter
|
||||
public class Utf8GamePathConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert( Type objectType )
|
||||
=> objectType == typeof( NewGamePath );
|
||||
=> objectType == typeof( Utf8GamePath );
|
||||
|
||||
public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer )
|
||||
{
|
||||
var token = JToken.Load( reader ).ToString();
|
||||
return FromString( token, out var p, true )
|
||||
? p
|
||||
: throw new JsonException( $"Could not convert \"{token}\" to {nameof( NewGamePath )}." );
|
||||
: throw new JsonException( $"Could not convert \"{token}\" to {nameof( Utf8GamePath )}." );
|
||||
}
|
||||
|
||||
public override bool CanWrite
|
||||
|
|
@ -150,10 +154,13 @@ public readonly struct NewGamePath : IEquatable< NewGamePath >, IComparable< New
|
|||
|
||||
public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer )
|
||||
{
|
||||
if( value is NewGamePath p )
|
||||
if( value is Utf8GamePath p )
|
||||
{
|
||||
serializer.Serialize( writer, p.ToString() );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public GamePath ToGamePath()
|
||||
=> GamePath.GenerateUnchecked( ToString() );
|
||||
}
|
||||
|
|
@ -1,23 +1,28 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using Dalamud.Utility;
|
||||
using Microsoft.VisualBasic.CompilerServices;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Penumbra.GameData.ByteString;
|
||||
|
||||
[JsonConverter( typeof( NewRelPathConverter ) )]
|
||||
public readonly struct NewRelPath : IEquatable< NewRelPath >, IComparable< NewRelPath >, IDisposable
|
||||
[JsonConverter( typeof( Utf8RelPathConverter ) )]
|
||||
public readonly struct Utf8RelPath : IEquatable< Utf8RelPath >, IComparable< Utf8RelPath >, IDisposable
|
||||
{
|
||||
public const int MaxRelPathLength = 250;
|
||||
|
||||
public readonly Utf8String Path;
|
||||
public static readonly NewRelPath Empty = new(Utf8String.Empty);
|
||||
public readonly Utf8String Path;
|
||||
public static readonly Utf8RelPath Empty = new(Utf8String.Empty);
|
||||
|
||||
internal NewRelPath( Utf8String path )
|
||||
internal Utf8RelPath( Utf8String path )
|
||||
=> Path = path;
|
||||
|
||||
public static bool FromString( string? s, out NewRelPath path )
|
||||
|
||||
public static explicit operator Utf8RelPath( string s )
|
||||
=> FromString( s, out var p ) ? p : Empty;
|
||||
|
||||
public static bool FromString( string? s, out Utf8RelPath path )
|
||||
{
|
||||
path = Empty;
|
||||
if( s.IsNullOrEmpty() )
|
||||
|
|
@ -42,11 +47,11 @@ public readonly struct NewRelPath : IEquatable< NewRelPath >, IComparable< NewRe
|
|||
return false;
|
||||
}
|
||||
|
||||
path = new NewRelPath( ascii );
|
||||
path = new Utf8RelPath( ascii );
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool FromFile( FileInfo file, DirectoryInfo baseDir, out NewRelPath path )
|
||||
public static bool FromFile( FileInfo file, DirectoryInfo baseDir, out Utf8RelPath path )
|
||||
{
|
||||
path = Empty;
|
||||
if( !file.FullName.StartsWith( baseDir.FullName ) )
|
||||
|
|
@ -58,7 +63,7 @@ public readonly struct NewRelPath : IEquatable< NewRelPath >, IComparable< NewRe
|
|||
return FromString( substring, out path );
|
||||
}
|
||||
|
||||
public static bool FromFile( FullPath file, DirectoryInfo baseDir, out NewRelPath path )
|
||||
public static bool FromFile( FullPath file, DirectoryInfo baseDir, out Utf8RelPath path )
|
||||
{
|
||||
path = Empty;
|
||||
if( !file.FullName.StartsWith( baseDir.FullName ) )
|
||||
|
|
@ -70,10 +75,10 @@ public readonly struct NewRelPath : IEquatable< NewRelPath >, IComparable< NewRe
|
|||
return FromString( substring, out path );
|
||||
}
|
||||
|
||||
public NewRelPath( NewGamePath gamePath )
|
||||
public Utf8RelPath( Utf8GamePath gamePath )
|
||||
=> Path = gamePath.Path.Replace( ( byte )'/', ( byte )'\\' );
|
||||
|
||||
public unsafe NewGamePath ToGamePath( int skipFolders = 0 )
|
||||
public unsafe Utf8GamePath ToGamePath( int skipFolders = 0 )
|
||||
{
|
||||
var idx = 0;
|
||||
while( skipFolders > 0 )
|
||||
|
|
@ -82,7 +87,7 @@ public readonly struct NewRelPath : IEquatable< NewRelPath >, IComparable< NewRe
|
|||
--skipFolders;
|
||||
if( idx <= 0 )
|
||||
{
|
||||
return NewGamePath.Empty;
|
||||
return Utf8GamePath.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -91,13 +96,13 @@ public readonly struct NewRelPath : IEquatable< NewRelPath >, IComparable< NewRe
|
|||
ByteStringFunctions.Replace( ptr, length, ( byte )'\\', ( byte )'/' );
|
||||
ByteStringFunctions.AsciiToLowerInPlace( ptr, length );
|
||||
var utf = new Utf8String().Setup( ptr, length, null, true, true, true, true );
|
||||
return new NewGamePath( utf );
|
||||
return new Utf8GamePath( utf );
|
||||
}
|
||||
|
||||
public int CompareTo( NewRelPath rhs )
|
||||
public int CompareTo( Utf8RelPath rhs )
|
||||
=> Path.CompareTo( rhs.Path );
|
||||
|
||||
public bool Equals( NewRelPath other )
|
||||
public bool Equals( Utf8RelPath other )
|
||||
=> Path.Equals( other.Path );
|
||||
|
||||
public override string ToString()
|
||||
|
|
@ -106,17 +111,17 @@ public readonly struct NewRelPath : IEquatable< NewRelPath >, IComparable< NewRe
|
|||
public void Dispose()
|
||||
=> Path.Dispose();
|
||||
|
||||
private class NewRelPathConverter : JsonConverter
|
||||
public class Utf8RelPathConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert( Type objectType )
|
||||
=> objectType == typeof( NewRelPath );
|
||||
=> objectType == typeof( Utf8RelPath );
|
||||
|
||||
public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer )
|
||||
{
|
||||
var token = JToken.Load( reader ).ToString();
|
||||
return FromString( token, out var p )
|
||||
? p
|
||||
: throw new JsonException( $"Could not convert \"{token}\" to {nameof( NewRelPath )}." );
|
||||
: throw new JsonException( $"Could not convert \"{token}\" to {nameof( Utf8RelPath )}." );
|
||||
}
|
||||
|
||||
public override bool CanWrite
|
||||
|
|
@ -124,7 +129,7 @@ public readonly struct NewRelPath : IEquatable< NewRelPath >, IComparable< NewRe
|
|||
|
||||
public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer )
|
||||
{
|
||||
if( value is NewRelPath p )
|
||||
if( value is Utf8RelPath p )
|
||||
{
|
||||
serializer.Serialize( writer, p.ToString() );
|
||||
}
|
||||
|
|
@ -75,6 +75,19 @@ public sealed unsafe partial class Utf8String : IEquatable< Utf8String >, ICompa
|
|||
return ByteStringFunctions.AsciiCaselessCompare( _path, Length, other._path, other.Length );
|
||||
}
|
||||
|
||||
public bool StartsWith( Utf8String other )
|
||||
{
|
||||
var otherLength = other.Length;
|
||||
return otherLength <= Length && ByteStringFunctions.Equals( other.Path, otherLength, Path, otherLength );
|
||||
}
|
||||
|
||||
public bool EndsWith( Utf8String other )
|
||||
{
|
||||
var otherLength = other.Length;
|
||||
var offset = Length - otherLength;
|
||||
return offset >= 0 && ByteStringFunctions.Equals( other.Path, otherLength, Path + offset, otherLength );
|
||||
}
|
||||
|
||||
public bool StartsWith( params char[] chars )
|
||||
{
|
||||
if( chars.Length > Length )
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ public class ModsController : WebApiController
|
|||
public object GetFiles()
|
||||
{
|
||||
return Penumbra.ModManager.Collections.CurrentCollection.Cache?.ResolvedFiles.ToDictionary(
|
||||
o => ( string )o.Key,
|
||||
o => o.Key.ToString(),
|
||||
o => o.Value.FullName
|
||||
)
|
||||
?? new Dictionary< string, string >();
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using System.Reflection;
|
|||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Logging;
|
||||
using Lumina.Data;
|
||||
using Penumbra.GameData.ByteString;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Util;
|
||||
using Penumbra.Mods;
|
||||
|
|
@ -78,16 +79,15 @@ public class PenumbraApi : IDisposable, IPenumbraApi
|
|||
|
||||
private static string ResolvePath( string path, ModManager manager, ModCollection collection )
|
||||
{
|
||||
if( !Penumbra.Config.IsEnabled )
|
||||
if( !Penumbra.Config.EnableMods )
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
var gamePath = new GamePath( path );
|
||||
var gamePath = Utf8GamePath.FromString( path, out var p, true ) ? p : Utf8GamePath.Empty;
|
||||
var ret = collection.Cache?.ResolveSwappedOrReplacementPath( gamePath );
|
||||
ret ??= manager.Collections.ForcedCollection.Cache?.ResolveSwappedOrReplacementPath( gamePath );
|
||||
ret ??= path;
|
||||
return ret;
|
||||
return ret?.ToString() ?? path;
|
||||
}
|
||||
|
||||
public string ResolvePath( string path )
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Numerics;
|
||||
using Dalamud.Configuration;
|
||||
using Dalamud.Logging;
|
||||
|
||||
|
|
@ -14,13 +12,19 @@ public class Configuration : IPluginConfiguration
|
|||
|
||||
public int Version { get; set; } = CurrentVersion;
|
||||
|
||||
public bool IsEnabled { get; set; } = true;
|
||||
public bool EnableMods { get; set; } = true;
|
||||
#if DEBUG
|
||||
public bool DebugMode { get; set; } = true;
|
||||
#else
|
||||
public bool DebugMode { get; set; } = false;
|
||||
#endif
|
||||
|
||||
public bool EnableFullResourceLogging { get; set; } = false;
|
||||
public bool EnableResourceLogging { get; set; } = false;
|
||||
public string ResourceLoggingFilter { get; set; } = string.Empty;
|
||||
|
||||
public bool ScaleModSelector { get; set; } = false;
|
||||
|
||||
public bool ShowAdvanced { get; set; }
|
||||
|
||||
public bool DisableFileSystemNotifications { get; set; }
|
||||
|
|
|
|||
|
|
@ -8,14 +8,15 @@ using Dalamud.Game.Gui;
|
|||
using Dalamud.Interface;
|
||||
using Dalamud.IoC;
|
||||
using Dalamud.Plugin;
|
||||
|
||||
// ReSharper disable AutoPropertyCanBeMadeGetOnly.Local
|
||||
|
||||
namespace Penumbra
|
||||
namespace Penumbra;
|
||||
|
||||
public class Dalamud
|
||||
{
|
||||
public class Dalamud
|
||||
{
|
||||
public static void Initialize(DalamudPluginInterface pluginInterface)
|
||||
=> pluginInterface.Create<Dalamud>();
|
||||
public static void Initialize( DalamudPluginInterface pluginInterface )
|
||||
=> pluginInterface.Create< Dalamud >();
|
||||
|
||||
// @formatter:off
|
||||
[PluginService][RequiredVersion("1.0")] public static DalamudPluginInterface PluginInterface { get; private set; } = null!;
|
||||
|
|
@ -29,6 +30,5 @@ namespace Penumbra
|
|||
[PluginService][RequiredVersion("1.0")] public static TargetManager Targets { get; private set; } = null!;
|
||||
[PluginService][RequiredVersion("1.0")] public static ObjectTable Objects { get; private set; } = null!;
|
||||
[PluginService][RequiredVersion("1.0")] public static TitleScreenMenu TitleScreenMenu { get; private set; } = null!;
|
||||
// @formatter:on
|
||||
}
|
||||
// @formatter:on
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using Penumbra.Structs;
|
||||
using Penumbra.Mod;
|
||||
|
||||
namespace Penumbra.Importer.Models
|
||||
{
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@ using System.Text;
|
|||
using Dalamud.Logging;
|
||||
using ICSharpCode.SharpZipLib.Zip;
|
||||
using Newtonsoft.Json;
|
||||
using Penumbra.GameData.ByteString;
|
||||
using Penumbra.GameData.Util;
|
||||
using Penumbra.Importer.Models;
|
||||
using Penumbra.Mod;
|
||||
using Penumbra.Structs;
|
||||
using Penumbra.Util;
|
||||
using FileMode = System.IO.FileMode;
|
||||
|
||||
|
|
@ -336,14 +336,18 @@ internal class TexToolsImport
|
|||
{
|
||||
OptionName = opt.Name!,
|
||||
OptionDesc = string.IsNullOrEmpty( opt.Description ) ? "" : opt.Description!,
|
||||
OptionFiles = new Dictionary< RelPath, HashSet< GamePath > >(),
|
||||
OptionFiles = new Dictionary< Utf8RelPath, HashSet< Utf8GamePath > >(),
|
||||
};
|
||||
var optDir = NewOptionDirectory( groupFolder, opt.Name! );
|
||||
if( optDir.Exists )
|
||||
{
|
||||
foreach( var file in optDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) )
|
||||
{
|
||||
option.AddFile( new RelPath( file, baseFolder ), new GamePath( file, optDir ) );
|
||||
if( Utf8RelPath.FromFile( file, baseFolder, out var rel )
|
||||
&& Utf8GamePath.FromFile( file, optDir, out var game, true ) )
|
||||
{
|
||||
option.AddFile( rel, game );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ public unsafe partial class ResourceLoader
|
|||
{
|
||||
public ResourceHandle* OriginalResource;
|
||||
public ResourceHandle* ManipulatedResource;
|
||||
public NewGamePath OriginalPath;
|
||||
public Utf8GamePath OriginalPath;
|
||||
public FullPath ManipulatedPath;
|
||||
public ResourceCategory Category;
|
||||
public object? ResolverInfo;
|
||||
|
|
@ -44,7 +44,7 @@ public unsafe partial class ResourceLoader
|
|||
ResourceLoaded -= AddModifiedDebugInfo;
|
||||
}
|
||||
|
||||
private void AddModifiedDebugInfo( ResourceHandle* handle, NewGamePath originalPath, FullPath? manipulatedPath, object? resolverInfo )
|
||||
private void AddModifiedDebugInfo( ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, object? resolverInfo )
|
||||
{
|
||||
if( manipulatedPath == null )
|
||||
{
|
||||
|
|
@ -188,11 +188,11 @@ public unsafe partial class ResourceLoader
|
|||
}
|
||||
}
|
||||
|
||||
// Logging functions for EnableLogging.
|
||||
private static void LogPath( NewGamePath path, bool synchronous )
|
||||
=> PluginLog.Information( $"Requested {path} {( synchronous ? "synchronously." : "asynchronously." )}" );
|
||||
// Logging functions for EnableFullLogging.
|
||||
private static void LogPath( Utf8GamePath path, bool synchronous )
|
||||
=> PluginLog.Information( $"[ResourceLoader] Requested {path} {( synchronous ? "synchronously." : "asynchronously." )}" );
|
||||
|
||||
private static void LogResource( ResourceHandle* handle, NewGamePath path, FullPath? manipulatedPath, object? _ )
|
||||
private static void LogResource( ResourceHandle* handle, Utf8GamePath path, FullPath? manipulatedPath, object? _ )
|
||||
{
|
||||
var pathString = manipulatedPath != null ? $"custom file {manipulatedPath} instead of {path}" : path.ToString();
|
||||
PluginLog.Information( $"[ResourceLoader] Loaded {pathString} to 0x{( ulong )handle:X}. (Refcount {handle->RefCount})" );
|
||||
|
|
@ -200,6 +200,6 @@ public unsafe partial class ResourceLoader
|
|||
|
||||
private static void LogLoadedFile( Utf8String path, bool success, bool custom )
|
||||
=> PluginLog.Information( success
|
||||
? $"Loaded {path} from {( custom ? "local files" : "SqPack" )}"
|
||||
: $"Failed to load {path} from {( custom ? "local files" : "SqPack" )}." );
|
||||
? $"[ResourceLoader] Loaded {path} from {( custom ? "local files" : "SqPack" )}"
|
||||
: $"[ResourceLoader] Failed to load {path} from {( custom ? "local files" : "SqPack" )}." );
|
||||
}
|
||||
|
|
@ -46,7 +46,7 @@ public unsafe partial class ResourceLoader
|
|||
|
||||
|
||||
[Conditional( "DEBUG" )]
|
||||
private static void CompareHash( int local, int game, NewGamePath path )
|
||||
private static void CompareHash( int local, int game, Utf8GamePath path )
|
||||
{
|
||||
if( local != game )
|
||||
{
|
||||
|
|
@ -54,12 +54,12 @@ public unsafe partial class ResourceLoader
|
|||
}
|
||||
}
|
||||
|
||||
private event Action< NewGamePath, FullPath?, object? >? PathResolved;
|
||||
private event Action< Utf8GamePath, FullPath?, object? >? PathResolved;
|
||||
|
||||
private ResourceHandle* GetResourceHandler( bool isSync, ResourceManager* resourceManager, ResourceCategory* categoryId, uint* resourceType,
|
||||
int* resourceHash, byte* path, void* unk, bool isUnk )
|
||||
{
|
||||
if( !NewGamePath.FromPointer( path, out var gamePath ) )
|
||||
if( !Utf8GamePath.FromPointer( path, out var gamePath ) )
|
||||
{
|
||||
PluginLog.Error( "Could not create GamePath from resource path." );
|
||||
return CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, unk, isUnk );
|
||||
|
|
@ -114,7 +114,7 @@ public unsafe partial class ResourceLoader
|
|||
return ReadSqPackHook.Original( resourceManager, fileDescriptor, priority, isSync );
|
||||
}
|
||||
|
||||
var valid = NewGamePath.FromSpan( fileDescriptor->ResourceHandle->FileNameSpan(), out var gamePath, false );
|
||||
var valid = Utf8GamePath.FromSpan( fileDescriptor->ResourceHandle->FileNameSpan(), out var gamePath, false );
|
||||
byte ret;
|
||||
// The internal buffer size does not allow for more than 260 characters.
|
||||
// We use the IsRooted check to signify paths replaced by us pointing to the local filesystem instead of an SqPack.
|
||||
|
|
@ -151,11 +151,10 @@ public unsafe partial class ResourceLoader
|
|||
}
|
||||
|
||||
// Use the default method of path replacement.
|
||||
public static (FullPath?, object?) DefaultReplacer( NewGamePath path )
|
||||
public static (FullPath?, object?) DefaultReplacer( Utf8GamePath path )
|
||||
{
|
||||
var gamePath = new GamePath( path.ToString() );
|
||||
var resolved = Penumbra.ModManager.ResolveSwappedOrReplacementPath( gamePath );
|
||||
return resolved != null ? ( new FullPath( resolved ), null ) : ( null, null );
|
||||
var resolved = Penumbra.ModManager.ResolveSwappedOrReplacementPath( path );
|
||||
return( resolved, null );
|
||||
}
|
||||
|
||||
private void DisposeHooks()
|
||||
|
|
|
|||
|
|
@ -4,8 +4,6 @@ using Dalamud.Hooking;
|
|||
using Dalamud.Utility.Signatures;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
|
||||
using Penumbra.GameData.ByteString;
|
||||
using Penumbra.GameData.Util;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.Interop;
|
||||
|
||||
|
|
@ -69,7 +67,7 @@ public unsafe partial class ResourceLoader
|
|||
: LoadMdlFileExternHook.Original( resourceHandle, unk1, unk2, ptr );
|
||||
|
||||
|
||||
private void AddCrc( NewGamePath _, FullPath? path, object? _2 )
|
||||
private void AddCrc( Utf8GamePath _, FullPath? path, object? _2 )
|
||||
{
|
||||
if( path is { Extension: ".mdl" or ".tex" } p )
|
||||
{
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ public unsafe partial class ResourceLoader : IDisposable
|
|||
// Events can be used to make smarter logging.
|
||||
public bool IsLoggingEnabled { get; private set; }
|
||||
|
||||
public void EnableLogging()
|
||||
public void EnableFullLogging()
|
||||
{
|
||||
if( IsLoggingEnabled )
|
||||
{
|
||||
|
|
@ -31,7 +31,7 @@ public unsafe partial class ResourceLoader : IDisposable
|
|||
EnableHooks();
|
||||
}
|
||||
|
||||
public void DisableLogging()
|
||||
public void DisableFullLogging()
|
||||
{
|
||||
if( !IsLoggingEnabled )
|
||||
{
|
||||
|
|
@ -99,13 +99,13 @@ public unsafe partial class ResourceLoader : IDisposable
|
|||
}
|
||||
|
||||
// Event fired whenever a resource is requested.
|
||||
public delegate void ResourceRequestedDelegate( NewGamePath path, bool synchronous );
|
||||
public delegate void ResourceRequestedDelegate( Utf8GamePath path, bool synchronous );
|
||||
public event ResourceRequestedDelegate? ResourceRequested;
|
||||
|
||||
// Event fired whenever a resource is returned.
|
||||
// If the path was manipulated by penumbra, manipulatedPath will be the file path of the loaded resource.
|
||||
// resolveData is additional data returned by the current ResolvePath function and is user-defined.
|
||||
public delegate void ResourceLoadedDelegate( ResourceHandle* handle, NewGamePath originalPath, FullPath? manipulatedPath,
|
||||
public delegate void ResourceLoadedDelegate( ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath,
|
||||
object? resolveData );
|
||||
|
||||
public event ResourceLoadedDelegate? ResourceLoaded;
|
||||
|
|
@ -118,10 +118,11 @@ public unsafe partial class ResourceLoader : IDisposable
|
|||
public event FileLoadedDelegate? FileLoaded;
|
||||
|
||||
// Customization point to control how path resolving is handled.
|
||||
public Func< NewGamePath, (FullPath?, object?) > ResolvePath { get; set; } = DefaultReplacer;
|
||||
public Func< Utf8GamePath, (FullPath?, object?) > ResolvePath { get; set; } = DefaultReplacer;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
DisableFullLogging();
|
||||
DisposeHooks();
|
||||
DisposeTexMdlTreatment();
|
||||
}
|
||||
|
|
|
|||
98
Penumbra/Interop/ResourceLogger.cs
Normal file
98
Penumbra/Interop/ResourceLogger.cs
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
using Dalamud.Logging;
|
||||
using Penumbra.GameData.ByteString;
|
||||
|
||||
namespace Penumbra.Interop;
|
||||
|
||||
// A logger class that contains the relevant data to log requested files via regex.
|
||||
// Filters are case-insensitive.
|
||||
public class ResourceLogger : IDisposable
|
||||
{
|
||||
// Enable or disable the logging of resources subject to the current filter.
|
||||
public void SetState( bool value )
|
||||
{
|
||||
if( value == Penumbra.Config.EnableResourceLogging )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Penumbra.Config.EnableResourceLogging = value;
|
||||
Penumbra.Config.Save();
|
||||
if( value )
|
||||
{
|
||||
_resourceLoader.ResourceRequested += OnResourceRequested;
|
||||
}
|
||||
else
|
||||
{
|
||||
_resourceLoader.ResourceRequested -= OnResourceRequested;
|
||||
}
|
||||
}
|
||||
|
||||
// Set the current filter to a new string, doing all other necessary work.
|
||||
public void SetFilter( string newFilter )
|
||||
{
|
||||
if( newFilter == Filter )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Penumbra.Config.ResourceLoggingFilter = newFilter;
|
||||
Penumbra.Config.Save();
|
||||
SetupRegex();
|
||||
}
|
||||
|
||||
// Returns whether the current filter is a valid regular expression.
|
||||
public bool ValidRegex
|
||||
=> _filterRegex != null;
|
||||
|
||||
private readonly ResourceLoader _resourceLoader;
|
||||
private Regex? _filterRegex;
|
||||
|
||||
private static string Filter
|
||||
=> Penumbra.Config.ResourceLoggingFilter;
|
||||
|
||||
private void SetupRegex()
|
||||
{
|
||||
try
|
||||
{
|
||||
_filterRegex = new Regex( Filter, RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant );
|
||||
}
|
||||
catch
|
||||
{
|
||||
_filterRegex = null;
|
||||
}
|
||||
}
|
||||
|
||||
public ResourceLogger( ResourceLoader loader )
|
||||
{
|
||||
_resourceLoader = loader;
|
||||
SetupRegex();
|
||||
if( Penumbra.Config.EnableResourceLogging )
|
||||
{
|
||||
_resourceLoader.ResourceRequested += OnResourceRequested;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnResourceRequested( Utf8GamePath data, bool synchronous )
|
||||
{
|
||||
var path = Match( data.Path );
|
||||
if( path != null )
|
||||
{
|
||||
PluginLog.Information( $"{path} was requested {( synchronous ? "synchronously." : "asynchronously." )}" );
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the converted string if the filter matches, and null otherwise.
|
||||
// The filter matches if it is empty, if it is a valid and matching regex or if the given string contains it.
|
||||
private string? Match( Utf8String data )
|
||||
{
|
||||
var s = data.ToString();
|
||||
return Filter.Length == 0 || ( _filterRegex?.IsMatch( s ) ?? s.Contains( Filter, StringComparison.InvariantCultureIgnoreCase ) )
|
||||
? s
|
||||
: null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
=> _resourceLoader.ResourceRequested -= OnResourceRequested;
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
using System.Runtime.InteropServices;
|
||||
using Penumbra.Structs;
|
||||
|
||||
namespace Penumbra.Interop.Structs;
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ using Penumbra.GameData.ByteString;
|
|||
using Penumbra.Importer;
|
||||
using Penumbra.Meta.Files;
|
||||
using Penumbra.Mod;
|
||||
using Penumbra.Structs;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.Meta;
|
||||
|
|
@ -167,14 +166,14 @@ public class MetaCollection
|
|||
continue;
|
||||
}
|
||||
|
||||
var path = new RelPath( file, basePath );
|
||||
Utf8RelPath.FromFile( file, basePath, out var path );
|
||||
var foundAny = false;
|
||||
foreach( var group in modMeta.Groups )
|
||||
foreach( var (name, group) in modMeta.Groups )
|
||||
{
|
||||
foreach( var option in group.Value.Options.Where( o => o.OptionFiles.ContainsKey( path ) ) )
|
||||
foreach( var option in group.Options.Where( o => o.OptionFiles.ContainsKey( path ) ) )
|
||||
{
|
||||
foundAny = true;
|
||||
AddMeta( group.Key, option.OptionName, metaData );
|
||||
AddMeta( name, option.OptionName, metaData );
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ using Dalamud.Logging;
|
|||
using Lumina.Data.Files;
|
||||
using Penumbra.GameData.ByteString;
|
||||
using Penumbra.GameData.Util;
|
||||
using Penumbra.Interop;
|
||||
using Penumbra.Meta.Files;
|
||||
using Penumbra.Util;
|
||||
|
||||
|
|
@ -24,7 +23,7 @@ public class MetaManager : IDisposable
|
|||
public FileInformation( object data )
|
||||
=> Data = data;
|
||||
|
||||
public void Write( DirectoryInfo dir, GamePath originalPath )
|
||||
public void Write( DirectoryInfo dir, Utf8GamePath originalPath )
|
||||
{
|
||||
ByteData = Data switch
|
||||
{
|
||||
|
|
@ -44,16 +43,16 @@ public class MetaManager : IDisposable
|
|||
|
||||
public const string TmpDirectory = "penumbrametatmp";
|
||||
|
||||
private readonly DirectoryInfo _dir;
|
||||
private readonly Dictionary< GamePath, FullPath > _resolvedFiles;
|
||||
private readonly DirectoryInfo _dir;
|
||||
private readonly Dictionary< Utf8GamePath, FullPath > _resolvedFiles;
|
||||
|
||||
private readonly Dictionary< MetaManipulation, Mod.Mod > _currentManipulations = new();
|
||||
private readonly Dictionary< GamePath, FileInformation > _currentFiles = new();
|
||||
private readonly Dictionary< MetaManipulation, Mod.Mod > _currentManipulations = new();
|
||||
private readonly Dictionary< Utf8GamePath, FileInformation > _currentFiles = new();
|
||||
|
||||
public IEnumerable< (MetaManipulation, Mod.Mod) > Manipulations
|
||||
=> _currentManipulations.Select( kvp => ( kvp.Key, kvp.Value ) );
|
||||
|
||||
public IEnumerable< (GamePath, FullPath) > Files
|
||||
public IEnumerable< (Utf8GamePath, FullPath) > Files
|
||||
=> _currentFiles.Where( kvp => kvp.Value.CurrentFile != null )
|
||||
.Select( kvp => ( kvp.Key, kvp.Value.CurrentFile!.Value ) );
|
||||
|
||||
|
|
@ -121,7 +120,7 @@ public class MetaManager : IDisposable
|
|||
private void ClearDirectory()
|
||||
=> ClearDirectory( _dir );
|
||||
|
||||
public MetaManager( string name, Dictionary< GamePath, FullPath > resolvedFiles, DirectoryInfo tempDir )
|
||||
public MetaManager( string name, Dictionary< Utf8GamePath, FullPath > resolvedFiles, DirectoryInfo tempDir )
|
||||
{
|
||||
_resolvedFiles = resolvedFiles;
|
||||
_dir = new DirectoryInfo( Path.Combine( tempDir.FullName, name.ReplaceBadXivSymbols() ) );
|
||||
|
|
@ -135,13 +134,13 @@ public class MetaManager : IDisposable
|
|||
Directory.CreateDirectory( _dir.FullName );
|
||||
}
|
||||
|
||||
foreach( var kvp in _currentFiles.Where( kvp => kvp.Value.Changed ) )
|
||||
foreach( var (key, value) in _currentFiles.Where( kvp => kvp.Value.Changed ) )
|
||||
{
|
||||
kvp.Value.Write( _dir, kvp.Key );
|
||||
_resolvedFiles[ kvp.Key ] = kvp.Value.CurrentFile!.Value;
|
||||
if( kvp.Value.Data is EqpFile )
|
||||
value.Write( _dir, key );
|
||||
_resolvedFiles[ key ] = value.CurrentFile!.Value;
|
||||
if( value.Data is EqpFile )
|
||||
{
|
||||
EqpData = kvp.Value.ByteData;
|
||||
EqpData = value.ByteData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -154,7 +153,7 @@ public class MetaManager : IDisposable
|
|||
}
|
||||
|
||||
_currentManipulations.Add( m, mod );
|
||||
var gamePath = m.CorrespondingFilename();
|
||||
var gamePath = Utf8GamePath.FromString(m.CorrespondingFilename(), out var p, false) ? p : Utf8GamePath.Empty; // TODO
|
||||
try
|
||||
{
|
||||
if( !_currentFiles.TryGetValue( gamePath, out var file ) )
|
||||
|
|
|
|||
|
|
@ -3,84 +3,81 @@ using System.Collections.Generic;
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using Dalamud.Logging;
|
||||
using Dalamud.Plugin;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Penumbra.Mod;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra
|
||||
namespace Penumbra;
|
||||
|
||||
public static class MigrateConfiguration
|
||||
{
|
||||
public static class MigrateConfiguration
|
||||
public static void Version0To1( Configuration config )
|
||||
{
|
||||
public static void Version0To1( Configuration config )
|
||||
if( config.Version != 0 )
|
||||
{
|
||||
if( config.Version != 0 )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
config.ModDirectory = config.CurrentCollection;
|
||||
config.CurrentCollection = "Default";
|
||||
config.DefaultCollection = "Default";
|
||||
config.Version = 1;
|
||||
ResettleCollectionJson( config );
|
||||
return;
|
||||
}
|
||||
|
||||
private static void ResettleCollectionJson( Configuration config )
|
||||
config.ModDirectory = config.CurrentCollection;
|
||||
config.CurrentCollection = "Default";
|
||||
config.DefaultCollection = "Default";
|
||||
config.Version = 1;
|
||||
ResettleCollectionJson( config );
|
||||
}
|
||||
|
||||
private static void ResettleCollectionJson( Configuration config )
|
||||
{
|
||||
var collectionJson = new FileInfo( Path.Combine( config.ModDirectory, "collection.json" ) );
|
||||
if( !collectionJson.Exists )
|
||||
{
|
||||
var collectionJson = new FileInfo( Path.Combine( config.ModDirectory, "collection.json" ) );
|
||||
if( !collectionJson.Exists )
|
||||
{
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var defaultCollection = new ModCollection();
|
||||
var defaultCollectionFile = defaultCollection.FileName();
|
||||
if( defaultCollectionFile.Exists )
|
||||
{
|
||||
return;
|
||||
}
|
||||
var defaultCollection = new ModCollection();
|
||||
var defaultCollectionFile = defaultCollection.FileName();
|
||||
if( defaultCollectionFile.Exists )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var text = File.ReadAllText( collectionJson.FullName );
|
||||
var data = JArray.Parse( text );
|
||||
try
|
||||
{
|
||||
var text = File.ReadAllText( collectionJson.FullName );
|
||||
var data = JArray.Parse( text );
|
||||
|
||||
var maxPriority = 0;
|
||||
foreach( var setting in data.Cast< JObject >() )
|
||||
var maxPriority = 0;
|
||||
foreach( var setting in data.Cast< JObject >() )
|
||||
{
|
||||
var modName = ( string )setting[ "FolderName" ]!;
|
||||
var enabled = ( bool )setting[ "Enabled" ]!;
|
||||
var priority = ( int )setting[ "Priority" ]!;
|
||||
var settings = setting[ "Settings" ]!.ToObject< Dictionary< string, int > >()
|
||||
?? setting[ "Conf" ]!.ToObject< Dictionary< string, int > >();
|
||||
|
||||
var save = new ModSettings()
|
||||
{
|
||||
var modName = ( string )setting[ "FolderName" ]!;
|
||||
var enabled = ( bool )setting[ "Enabled" ]!;
|
||||
var priority = ( int )setting[ "Priority" ]!;
|
||||
var settings = setting[ "Settings" ]!.ToObject< Dictionary< string, int > >()
|
||||
?? setting[ "Conf" ]!.ToObject< Dictionary< string, int > >();
|
||||
|
||||
var save = new ModSettings()
|
||||
{
|
||||
Enabled = enabled,
|
||||
Priority = priority,
|
||||
Settings = settings!,
|
||||
};
|
||||
defaultCollection.Settings.Add( modName, save );
|
||||
maxPriority = Math.Max( maxPriority, priority );
|
||||
}
|
||||
|
||||
if( !config.InvertModListOrder )
|
||||
{
|
||||
foreach( var setting in defaultCollection.Settings.Values )
|
||||
{
|
||||
setting.Priority = maxPriority - setting.Priority;
|
||||
}
|
||||
}
|
||||
|
||||
defaultCollection.Save();
|
||||
Enabled = enabled,
|
||||
Priority = priority,
|
||||
Settings = settings!,
|
||||
};
|
||||
defaultCollection.Settings.Add( modName, save );
|
||||
maxPriority = Math.Max( maxPriority, priority );
|
||||
}
|
||||
catch( Exception e )
|
||||
|
||||
if( !config.InvertModListOrder )
|
||||
{
|
||||
PluginLog.Error( $"Could not migrate the old collection file to new collection files:\n{e}" );
|
||||
throw;
|
||||
foreach( var setting in defaultCollection.Settings.Values )
|
||||
{
|
||||
setting.Priority = maxPriority - setting.Priority;
|
||||
}
|
||||
}
|
||||
|
||||
defaultCollection.Save();
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
PluginLog.Error( $"Could not migrate the old collection file to new collection files:\n{e}" );
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
102
Penumbra/Mod/GroupInformation.cs
Normal file
102
Penumbra/Mod/GroupInformation.cs
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using Newtonsoft.Json;
|
||||
using Penumbra.GameData.ByteString;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.Mod;
|
||||
|
||||
public enum SelectType
|
||||
{
|
||||
Single,
|
||||
Multi,
|
||||
}
|
||||
|
||||
public struct Option
|
||||
{
|
||||
public string OptionName;
|
||||
public string OptionDesc;
|
||||
|
||||
[JsonProperty( ItemConverterType = typeof( SingleOrArrayConverter< Utf8GamePath > ) )]
|
||||
public Dictionary< Utf8RelPath, HashSet< Utf8GamePath > > OptionFiles;
|
||||
|
||||
public bool AddFile( Utf8RelPath filePath, Utf8GamePath gamePath )
|
||||
{
|
||||
if( OptionFiles.TryGetValue( filePath, out var set ) )
|
||||
{
|
||||
return set.Add( gamePath );
|
||||
}
|
||||
|
||||
OptionFiles[ filePath ] = new HashSet< Utf8GamePath > { gamePath };
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public struct OptionGroup
|
||||
{
|
||||
public string GroupName;
|
||||
|
||||
[JsonConverter( typeof( Newtonsoft.Json.Converters.StringEnumConverter ) )]
|
||||
public SelectType SelectionType;
|
||||
|
||||
public List< Option > Options;
|
||||
|
||||
private bool ApplySingleGroupFiles( Utf8RelPath relPath, int selection, HashSet< Utf8GamePath > paths )
|
||||
{
|
||||
// Selection contains the path, merge all GamePaths for this config.
|
||||
if( Options[ selection ].OptionFiles.TryGetValue( relPath, out var groupPaths ) )
|
||||
{
|
||||
paths.UnionWith( groupPaths );
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the group contains the file in another selection, return true to skip it for default files.
|
||||
for( var i = 0; i < Options.Count; ++i )
|
||||
{
|
||||
if( i == selection )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if( Options[ i ].OptionFiles.ContainsKey( relPath ) )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool ApplyMultiGroupFiles( Utf8RelPath relPath, int selection, HashSet< Utf8GamePath > paths )
|
||||
{
|
||||
var doNotAdd = false;
|
||||
for( var i = 0; i < Options.Count; ++i )
|
||||
{
|
||||
if( ( selection & ( 1 << i ) ) != 0 )
|
||||
{
|
||||
if( Options[ i ].OptionFiles.TryGetValue( relPath, out var groupPaths ) )
|
||||
{
|
||||
paths.UnionWith( groupPaths );
|
||||
doNotAdd = true;
|
||||
}
|
||||
}
|
||||
else if( Options[ i ].OptionFiles.ContainsKey( relPath ) )
|
||||
{
|
||||
doNotAdd = true;
|
||||
}
|
||||
}
|
||||
|
||||
return doNotAdd;
|
||||
}
|
||||
|
||||
// Adds all game paths from the given option that correspond to the given RelPath to paths, if any exist.
|
||||
internal bool ApplyGroupFiles( Utf8RelPath relPath, int selection, HashSet< Utf8GamePath > paths )
|
||||
{
|
||||
return SelectionType switch
|
||||
{
|
||||
SelectType.Single => ApplySingleGroupFiles( relPath, selection, paths ),
|
||||
SelectType.Multi => ApplyMultiGroupFiles( relPath, selection, paths ),
|
||||
_ => throw new InvalidEnumArgumentException( "Invalid option group type." ),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,35 +1,33 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Penumbra.GameData.Util;
|
||||
using Penumbra.Util;
|
||||
using Penumbra.GameData.ByteString;
|
||||
|
||||
namespace Penumbra.Mod
|
||||
namespace Penumbra.Mod;
|
||||
|
||||
// A complete Mod containing settings (i.e. dependent on a collection)
|
||||
// and the resulting cache.
|
||||
public class Mod
|
||||
{
|
||||
// A complete Mod containing settings (i.e. dependent on a collection)
|
||||
// and the resulting cache.
|
||||
public class Mod
|
||||
public ModSettings Settings { get; }
|
||||
public ModData Data { get; }
|
||||
public ModCache Cache { get; }
|
||||
|
||||
public Mod( ModSettings settings, ModData data )
|
||||
{
|
||||
public ModSettings Settings { get; }
|
||||
public ModData Data { get; }
|
||||
public ModCache Cache { get; }
|
||||
|
||||
public Mod( ModSettings settings, ModData data )
|
||||
{
|
||||
Settings = settings;
|
||||
Data = data;
|
||||
Cache = new ModCache();
|
||||
}
|
||||
|
||||
public bool FixSettings()
|
||||
=> Settings.FixInvalidSettings( Data.Meta );
|
||||
|
||||
public HashSet< GamePath > GetFiles( FileInfo file )
|
||||
{
|
||||
var relPath = new RelPath( file, Data.BasePath );
|
||||
return ModFunctions.GetFilesForConfig( relPath, Settings, Data.Meta );
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
=> Data.Meta.Name;
|
||||
Settings = settings;
|
||||
Data = data;
|
||||
Cache = new ModCache();
|
||||
}
|
||||
|
||||
public bool FixSettings()
|
||||
=> Settings.FixInvalidSettings( Data.Meta );
|
||||
|
||||
public HashSet< Utf8GamePath > GetFiles( FileInfo file )
|
||||
{
|
||||
var relPath = Utf8RelPath.FromFile( file, Data.BasePath, out var p ) ? p : Utf8RelPath.Empty;
|
||||
return ModFunctions.GetFilesForConfig( relPath, Settings, Data.Meta );
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
=> Data.Meta.Name;
|
||||
}
|
||||
|
|
@ -1,58 +1,57 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Penumbra.GameData.Util;
|
||||
using Penumbra.GameData.ByteString;
|
||||
using Penumbra.Meta;
|
||||
|
||||
namespace Penumbra.Mod
|
||||
namespace Penumbra.Mod;
|
||||
|
||||
// The ModCache contains volatile information dependent on all current settings in a collection.
|
||||
public class ModCache
|
||||
{
|
||||
// The ModCache contains volatile information dependent on all current settings in a collection.
|
||||
public class ModCache
|
||||
public Dictionary< Mod, (List< Utf8GamePath > Files, List< MetaManipulation > Manipulations) > Conflicts { get; private set; } = new();
|
||||
|
||||
public void AddConflict( Mod precedingMod, Utf8GamePath gamePath )
|
||||
{
|
||||
public Dictionary< Mod, (List< GamePath > Files, List< MetaManipulation > Manipulations) > Conflicts { get; private set; } = new();
|
||||
|
||||
public void AddConflict( Mod precedingMod, GamePath gamePath )
|
||||
if( Conflicts.TryGetValue( precedingMod, out var conflicts ) && !conflicts.Files.Contains( gamePath ) )
|
||||
{
|
||||
if( Conflicts.TryGetValue( precedingMod, out var conflicts ) && !conflicts.Files.Contains( gamePath ) )
|
||||
{
|
||||
conflicts.Files.Add( gamePath );
|
||||
}
|
||||
else
|
||||
{
|
||||
Conflicts[ precedingMod ] = ( new List< GamePath > { gamePath }, new List< MetaManipulation >() );
|
||||
}
|
||||
conflicts.Files.Add( gamePath );
|
||||
}
|
||||
|
||||
public void AddConflict( Mod precedingMod, MetaManipulation manipulation )
|
||||
else
|
||||
{
|
||||
if( Conflicts.TryGetValue( precedingMod, out var conflicts ) && !conflicts.Manipulations.Contains( manipulation ) )
|
||||
{
|
||||
conflicts.Manipulations.Add( manipulation );
|
||||
}
|
||||
else
|
||||
{
|
||||
Conflicts[ precedingMod ] = ( new List< GamePath >(), new List< MetaManipulation > { manipulation } );
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearConflicts()
|
||||
=> Conflicts.Clear();
|
||||
|
||||
public void ClearFileConflicts()
|
||||
{
|
||||
Conflicts = Conflicts.Where( kvp => kvp.Value.Manipulations.Count > 0 ).ToDictionary( kvp => kvp.Key, kvp =>
|
||||
{
|
||||
kvp.Value.Files.Clear();
|
||||
return kvp.Value;
|
||||
} );
|
||||
}
|
||||
|
||||
public void ClearMetaConflicts()
|
||||
{
|
||||
Conflicts = Conflicts.Where( kvp => kvp.Value.Files.Count > 0 ).ToDictionary( kvp => kvp.Key, kvp =>
|
||||
{
|
||||
kvp.Value.Manipulations.Clear();
|
||||
return kvp.Value;
|
||||
} );
|
||||
Conflicts[ precedingMod ] = ( new List< Utf8GamePath > { gamePath }, new List< MetaManipulation >() );
|
||||
}
|
||||
}
|
||||
|
||||
public void AddConflict( Mod precedingMod, MetaManipulation manipulation )
|
||||
{
|
||||
if( Conflicts.TryGetValue( precedingMod, out var conflicts ) && !conflicts.Manipulations.Contains( manipulation ) )
|
||||
{
|
||||
conflicts.Manipulations.Add( manipulation );
|
||||
}
|
||||
else
|
||||
{
|
||||
Conflicts[ precedingMod ] = ( new List< Utf8GamePath >(), new List< MetaManipulation > { manipulation } );
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearConflicts()
|
||||
=> Conflicts.Clear();
|
||||
|
||||
public void ClearFileConflicts()
|
||||
{
|
||||
Conflicts = Conflicts.Where( kvp => kvp.Value.Manipulations.Count > 0 ).ToDictionary( kvp => kvp.Key, kvp =>
|
||||
{
|
||||
kvp.Value.Files.Clear();
|
||||
return kvp.Value;
|
||||
} );
|
||||
}
|
||||
|
||||
public void ClearMetaConflicts()
|
||||
{
|
||||
Conflicts = Conflicts.Where( kvp => kvp.Value.Files.Count > 0 ).ToDictionary( kvp => kvp.Key, kvp =>
|
||||
{
|
||||
kvp.Value.Manipulations.Clear();
|
||||
return kvp.Value;
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
|
@ -2,526 +2,530 @@ using System;
|
|||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using Dalamud.Logging;
|
||||
using Penumbra.GameData.ByteString;
|
||||
using Penumbra.GameData.Util;
|
||||
using Penumbra.Importer;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Structs;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.Mod
|
||||
namespace Penumbra.Mod;
|
||||
|
||||
public class ModCleanup
|
||||
{
|
||||
public class ModCleanup
|
||||
private const string Duplicates = "Duplicates";
|
||||
private const string Required = "Required";
|
||||
|
||||
private readonly DirectoryInfo _baseDir;
|
||||
private readonly ModMeta _mod;
|
||||
private SHA256? _hasher;
|
||||
|
||||
private readonly Dictionary< long, List< FileInfo > > _filesBySize = new();
|
||||
|
||||
private SHA256 Sha()
|
||||
{
|
||||
private const string Duplicates = "Duplicates";
|
||||
private const string Required = "Required";
|
||||
_hasher ??= SHA256.Create();
|
||||
return _hasher;
|
||||
}
|
||||
|
||||
private ModCleanup( DirectoryInfo baseDir, ModMeta mod )
|
||||
{
|
||||
_baseDir = baseDir;
|
||||
_mod = mod;
|
||||
BuildDict();
|
||||
}
|
||||
|
||||
private readonly DirectoryInfo _baseDir;
|
||||
private readonly ModMeta _mod;
|
||||
private SHA256? _hasher;
|
||||
|
||||
private readonly Dictionary< long, List< FileInfo > > _filesBySize = new();
|
||||
|
||||
private SHA256 Sha()
|
||||
private void BuildDict()
|
||||
{
|
||||
foreach( var file in _baseDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) )
|
||||
{
|
||||
_hasher ??= SHA256.Create();
|
||||
return _hasher;
|
||||
}
|
||||
|
||||
private ModCleanup( DirectoryInfo baseDir, ModMeta mod )
|
||||
{
|
||||
_baseDir = baseDir;
|
||||
_mod = mod;
|
||||
BuildDict();
|
||||
}
|
||||
|
||||
private void BuildDict()
|
||||
{
|
||||
foreach( var file in _baseDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) )
|
||||
var fileLength = file.Length;
|
||||
if( _filesBySize.TryGetValue( fileLength, out var files ) )
|
||||
{
|
||||
var fileLength = file.Length;
|
||||
if( _filesBySize.TryGetValue( fileLength, out var files ) )
|
||||
{
|
||||
files.Add( file );
|
||||
}
|
||||
else
|
||||
{
|
||||
_filesBySize[ fileLength ] = new List< FileInfo >() { file };
|
||||
}
|
||||
files.Add( file );
|
||||
}
|
||||
else
|
||||
{
|
||||
_filesBySize[ fileLength ] = new List< FileInfo > { file };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static DirectoryInfo CreateNewModDir( ModData mod, string optionGroup, string option )
|
||||
{
|
||||
var newName = $"{mod.BasePath.Name}_{optionGroup}_{option}";
|
||||
var newDir = TexToolsImport.CreateModFolder( new DirectoryInfo( Penumbra.Config!.ModDirectory ), newName );
|
||||
return newDir;
|
||||
}
|
||||
private static DirectoryInfo CreateNewModDir( ModData mod, string optionGroup, string option )
|
||||
{
|
||||
var newName = $"{mod.BasePath.Name}_{optionGroup}_{option}";
|
||||
return TexToolsImport.CreateModFolder( new DirectoryInfo( Penumbra.Config.ModDirectory ), newName );
|
||||
}
|
||||
|
||||
private static ModData CreateNewMod( DirectoryInfo newDir, string newSortOrder )
|
||||
{
|
||||
Penumbra.ModManager.AddMod( newDir );
|
||||
var newMod = Penumbra.ModManager.Mods[ newDir.Name ];
|
||||
newMod.Move( newSortOrder );
|
||||
newMod.ComputeChangedItems();
|
||||
ModFileSystem.InvokeChange();
|
||||
return newMod;
|
||||
}
|
||||
private static ModData CreateNewMod( DirectoryInfo newDir, string newSortOrder )
|
||||
{
|
||||
Penumbra.ModManager.AddMod( newDir );
|
||||
var newMod = Penumbra.ModManager.Mods[ newDir.Name ];
|
||||
newMod.Move( newSortOrder );
|
||||
newMod.ComputeChangedItems();
|
||||
ModFileSystem.InvokeChange();
|
||||
return newMod;
|
||||
}
|
||||
|
||||
private static ModMeta CreateNewMeta( DirectoryInfo newDir, ModData mod, string name, string optionGroup, string option )
|
||||
private static ModMeta CreateNewMeta( DirectoryInfo newDir, ModData mod, string name, string optionGroup, string option )
|
||||
{
|
||||
var newMeta = new ModMeta
|
||||
{
|
||||
var newMeta = new ModMeta
|
||||
Author = mod.Meta.Author,
|
||||
Name = name,
|
||||
Description = $"Split from {mod.Meta.Name} Group {optionGroup} Option {option}.",
|
||||
};
|
||||
var metaFile = new FileInfo( Path.Combine( newDir.FullName, "meta.json" ) );
|
||||
newMeta.SaveToFile( metaFile );
|
||||
return newMeta;
|
||||
}
|
||||
|
||||
private static void CreateModSplit( HashSet< string > unseenPaths, ModData mod, OptionGroup group, Option option )
|
||||
{
|
||||
try
|
||||
{
|
||||
var newDir = CreateNewModDir( mod, group.GroupName, option.OptionName );
|
||||
var newName = group.SelectionType == SelectType.Multi ? $"{group.GroupName} - {option.OptionName}" : option.OptionName;
|
||||
var newMeta = CreateNewMeta( newDir, mod, newName, group.GroupName, option.OptionName );
|
||||
foreach( var (fileName, paths) in option.OptionFiles )
|
||||
{
|
||||
Author = mod.Meta.Author,
|
||||
Name = name,
|
||||
Description = $"Split from {mod.Meta.Name} Group {optionGroup} Option {option}.",
|
||||
};
|
||||
var metaFile = new FileInfo( Path.Combine( newDir.FullName, "meta.json" ) );
|
||||
newMeta.SaveToFile( metaFile );
|
||||
return newMeta;
|
||||
}
|
||||
|
||||
private static void CreateModSplit( HashSet< string > unseenPaths, ModData mod, OptionGroup group, Option option )
|
||||
{
|
||||
try
|
||||
{
|
||||
var newDir = CreateNewModDir( mod, group.GroupName!, option.OptionName );
|
||||
var newName = group.SelectionType == SelectType.Multi ? $"{group.GroupName} - {option.OptionName}" : option.OptionName;
|
||||
var newMeta = CreateNewMeta( newDir, mod, newName, group.GroupName!, option.OptionName );
|
||||
foreach( var (fileName, paths) in option.OptionFiles )
|
||||
var oldPath = Path.Combine( mod.BasePath.FullName, fileName.ToString() );
|
||||
unseenPaths.Remove( oldPath );
|
||||
if( File.Exists( oldPath ) )
|
||||
{
|
||||
var oldPath = Path.Combine( mod.BasePath.FullName, fileName );
|
||||
unseenPaths.Remove( oldPath );
|
||||
if( File.Exists( oldPath ) )
|
||||
foreach( var path in paths )
|
||||
{
|
||||
foreach( var path in paths )
|
||||
{
|
||||
var newPath = Path.Combine( newDir.FullName, path );
|
||||
Directory.CreateDirectory( Path.GetDirectoryName( newPath )! );
|
||||
File.Copy( oldPath, newPath, true );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var newSortOrder = group.SelectionType == SelectType.Single
|
||||
? $"{mod.SortOrder.ParentFolder.FullName}/{mod.Meta.Name}/{group.GroupName}/{option.OptionName}"
|
||||
: $"{mod.SortOrder.ParentFolder.FullName}/{mod.Meta.Name}/{group.GroupName} - {option.OptionName}";
|
||||
CreateNewMod( newDir, newSortOrder );
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
PluginLog.Error( $"Could not split Mod:\n{e}" );
|
||||
}
|
||||
}
|
||||
|
||||
public static void SplitMod( ModData mod )
|
||||
{
|
||||
if( !mod.Meta.Groups.Any() )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var unseenPaths = mod.Resources.ModFiles.Select( f => f.FullName ).ToHashSet();
|
||||
foreach( var group in mod.Meta.Groups.Values )
|
||||
{
|
||||
foreach( var option in group.Options )
|
||||
{
|
||||
CreateModSplit( unseenPaths, mod, group, option );
|
||||
}
|
||||
}
|
||||
|
||||
if( !unseenPaths.Any() )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var defaultGroup = new OptionGroup()
|
||||
{
|
||||
GroupName = "Default",
|
||||
SelectionType = SelectType.Multi,
|
||||
};
|
||||
var defaultOption = new Option()
|
||||
{
|
||||
OptionName = "Files",
|
||||
OptionFiles = unseenPaths.ToDictionary( p => new RelPath( new FileInfo( p ), mod.BasePath ),
|
||||
p => new HashSet< GamePath >() { new( new FileInfo( p ), mod.BasePath ) } ),
|
||||
};
|
||||
CreateModSplit( unseenPaths, mod, defaultGroup, defaultOption );
|
||||
}
|
||||
|
||||
private static Option FindOrCreateDuplicates( ModMeta meta )
|
||||
{
|
||||
static Option RequiredOption()
|
||||
=> new()
|
||||
{
|
||||
OptionName = Required,
|
||||
OptionDesc = "",
|
||||
OptionFiles = new Dictionary< RelPath, HashSet< GamePath > >(),
|
||||
};
|
||||
|
||||
if( meta.Groups.TryGetValue( Duplicates, out var duplicates ) )
|
||||
{
|
||||
var idx = duplicates.Options.FindIndex( o => o.OptionName == Required );
|
||||
if( idx >= 0 )
|
||||
{
|
||||
return duplicates.Options[ idx ];
|
||||
}
|
||||
|
||||
duplicates.Options.Add( RequiredOption() );
|
||||
return duplicates.Options.Last();
|
||||
}
|
||||
|
||||
meta.Groups.Add( Duplicates, new OptionGroup
|
||||
{
|
||||
GroupName = Duplicates,
|
||||
SelectionType = SelectType.Single,
|
||||
Options = new List< Option > { RequiredOption() },
|
||||
} );
|
||||
|
||||
return meta.Groups[ Duplicates ].Options.First();
|
||||
}
|
||||
|
||||
public static void Deduplicate( DirectoryInfo baseDir, ModMeta mod )
|
||||
{
|
||||
var dedup = new ModCleanup( baseDir, mod );
|
||||
foreach( var pair in dedup._filesBySize.Where( pair => pair.Value.Count >= 2 ) )
|
||||
{
|
||||
if( pair.Value.Count == 2 )
|
||||
{
|
||||
if( CompareFilesDirectly( pair.Value[ 0 ], pair.Value[ 1 ] ) )
|
||||
{
|
||||
dedup.ReplaceFile( pair.Value[ 0 ], pair.Value[ 1 ] );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var deleted = Enumerable.Repeat( false, pair.Value.Count ).ToArray();
|
||||
var hashes = pair.Value.Select( dedup.ComputeHash ).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 ] || !CompareHashes( hashes[ i ], hashes[ j ] ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
dedup.ReplaceFile( pair.Value[ i ], pair.Value[ j ] );
|
||||
deleted[ j ] = true;
|
||||
}
|
||||
var newPath = Path.Combine( newDir.FullName, path.ToString() );
|
||||
Directory.CreateDirectory( Path.GetDirectoryName( newPath )! );
|
||||
File.Copy( oldPath, newPath, true );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CleanUpDuplicates( mod );
|
||||
ClearEmptySubDirectories( dedup._baseDir );
|
||||
var newSortOrder = group.SelectionType == SelectType.Single
|
||||
? $"{mod.SortOrder.ParentFolder.FullName}/{mod.Meta.Name}/{group.GroupName}/{option.OptionName}"
|
||||
: $"{mod.SortOrder.ParentFolder.FullName}/{mod.Meta.Name}/{group.GroupName} - {option.OptionName}";
|
||||
CreateNewMod( newDir, newSortOrder );
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
PluginLog.Error( $"Could not split Mod:\n{e}" );
|
||||
}
|
||||
}
|
||||
|
||||
public static void SplitMod( ModData mod )
|
||||
{
|
||||
if( mod.Meta.Groups.Count == 0 )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
private void ReplaceFile( FileInfo f1, FileInfo f2 )
|
||||
var unseenPaths = mod.Resources.ModFiles.Select( f => f.FullName ).ToHashSet();
|
||||
foreach( var group in mod.Meta.Groups.Values )
|
||||
{
|
||||
RelPath relName1 = new( f1, _baseDir );
|
||||
RelPath relName2 = new( f2, _baseDir );
|
||||
|
||||
var inOption1 = false;
|
||||
var inOption2 = false;
|
||||
foreach( var option in _mod.Groups.SelectMany( g => g.Value.Options ) )
|
||||
foreach( var option in group.Options )
|
||||
{
|
||||
if( option.OptionFiles.ContainsKey( relName1 ) )
|
||||
{
|
||||
inOption1 = true;
|
||||
}
|
||||
|
||||
if( !option.OptionFiles.TryGetValue( relName2, out var values ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
inOption2 = true;
|
||||
|
||||
foreach( var value in values )
|
||||
{
|
||||
option.AddFile( relName1, value );
|
||||
}
|
||||
|
||||
option.OptionFiles.Remove( relName2 );
|
||||
}
|
||||
|
||||
if( !inOption1 || !inOption2 )
|
||||
{
|
||||
var duplicates = FindOrCreateDuplicates( _mod );
|
||||
if( !inOption1 )
|
||||
{
|
||||
duplicates.AddFile( relName1, relName2.ToGamePath() );
|
||||
}
|
||||
|
||||
if( !inOption2 )
|
||||
{
|
||||
duplicates.AddFile( relName1, relName1.ToGamePath() );
|
||||
}
|
||||
}
|
||||
|
||||
PluginLog.Information( $"File {relName1} and {relName2} are identical. Deleting the second." );
|
||||
f2.Delete();
|
||||
}
|
||||
|
||||
public static bool CompareFilesDirectly( FileInfo f1, FileInfo f2 )
|
||||
=> File.ReadAllBytes( f1.FullName ).SequenceEqual( File.ReadAllBytes( f2.FullName ) );
|
||||
|
||||
public static bool CompareHashes( byte[] f1, byte[] f2 )
|
||||
=> 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();
|
||||
}
|
||||
CreateModSplit( unseenPaths, mod, group, option );
|
||||
}
|
||||
}
|
||||
|
||||
private static bool FileIsInAnyGroup( ModMeta meta, RelPath relPath, bool exceptDuplicates = false )
|
||||
if( unseenPaths.Count == 0 )
|
||||
{
|
||||
var groupEnumerator = exceptDuplicates
|
||||
? meta.Groups.Values.Where( g => g.GroupName != Duplicates )
|
||||
: meta.Groups.Values;
|
||||
return groupEnumerator.SelectMany( group => group.Options )
|
||||
.Any( option => option.OptionFiles.ContainsKey( relPath ) );
|
||||
return;
|
||||
}
|
||||
|
||||
private static void CleanUpDuplicates( ModMeta meta )
|
||||
var defaultGroup = new OptionGroup()
|
||||
{
|
||||
if( !meta.Groups.TryGetValue( Duplicates, out var info ) )
|
||||
GroupName = "Default",
|
||||
SelectionType = SelectType.Multi,
|
||||
};
|
||||
var defaultOption = new Option()
|
||||
{
|
||||
OptionName = "Files",
|
||||
OptionFiles = unseenPaths.ToDictionary(
|
||||
p => Utf8RelPath.FromFile( new FileInfo( p ), mod.BasePath, out var rel ) ? rel : Utf8RelPath.Empty,
|
||||
p => new HashSet< Utf8GamePath >()
|
||||
{ Utf8GamePath.FromFile( new FileInfo( p ), mod.BasePath, out var game, true ) ? game : Utf8GamePath.Empty } ),
|
||||
};
|
||||
CreateModSplit( unseenPaths, mod, defaultGroup, defaultOption );
|
||||
}
|
||||
|
||||
private static Option FindOrCreateDuplicates( ModMeta meta )
|
||||
{
|
||||
static Option RequiredOption()
|
||||
=> new()
|
||||
{
|
||||
return;
|
||||
OptionName = Required,
|
||||
OptionDesc = "",
|
||||
OptionFiles = new Dictionary< Utf8RelPath, HashSet< Utf8GamePath > >(),
|
||||
};
|
||||
|
||||
if( meta.Groups.TryGetValue( Duplicates, out var duplicates ) )
|
||||
{
|
||||
var idx = duplicates.Options.FindIndex( o => o.OptionName == Required );
|
||||
if( idx >= 0 )
|
||||
{
|
||||
return duplicates.Options[ idx ];
|
||||
}
|
||||
|
||||
var requiredIdx = info.Options.FindIndex( o => o.OptionName == Required );
|
||||
if( requiredIdx >= 0 )
|
||||
duplicates.Options.Add( RequiredOption() );
|
||||
return duplicates.Options.Last();
|
||||
}
|
||||
|
||||
meta.Groups.Add( Duplicates, new OptionGroup
|
||||
{
|
||||
GroupName = Duplicates,
|
||||
SelectionType = SelectType.Single,
|
||||
Options = new List< Option > { RequiredOption() },
|
||||
} );
|
||||
|
||||
return meta.Groups[ Duplicates ].Options.First();
|
||||
}
|
||||
|
||||
public static void Deduplicate( DirectoryInfo baseDir, ModMeta mod )
|
||||
{
|
||||
var dedup = new ModCleanup( baseDir, mod );
|
||||
foreach( var (key, value) in dedup._filesBySize.Where( pair => pair.Value.Count >= 2 ) )
|
||||
{
|
||||
if( value.Count == 2 )
|
||||
{
|
||||
var required = info.Options[ requiredIdx ];
|
||||
foreach( var kvp in required.OptionFiles.ToArray() )
|
||||
if( CompareFilesDirectly( value[ 0 ], value[ 1 ] ) )
|
||||
{
|
||||
if( kvp.Value.Count > 1 || FileIsInAnyGroup( meta, kvp.Key, true ) )
|
||||
dedup.ReplaceFile( value[ 0 ], value[ 1 ] );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var deleted = Enumerable.Repeat( false, value.Count ).ToArray();
|
||||
var hashes = value.Select( dedup.ComputeHash ).ToArray();
|
||||
|
||||
for( var i = 0; i < value.Count; ++i )
|
||||
{
|
||||
if( deleted[ i ] )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if( kvp.Value.Count == 0 || kvp.Value.First().CompareTo( kvp.Key.ToGamePath() ) == 0 )
|
||||
for( var j = i + 1; j < value.Count; ++j )
|
||||
{
|
||||
required.OptionFiles.Remove( kvp.Key );
|
||||
if( deleted[ j ] || !CompareHashes( hashes[ i ], hashes[ j ] ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
dedup.ReplaceFile( value[ i ], value[ j ] );
|
||||
deleted[ j ] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if( required.OptionFiles.Count == 0 )
|
||||
{
|
||||
info.Options.RemoveAt( requiredIdx );
|
||||
}
|
||||
}
|
||||
|
||||
if( info.Options.Count == 0 )
|
||||
{
|
||||
meta.Groups.Remove( Duplicates );
|
||||
}
|
||||
}
|
||||
|
||||
public enum GroupType
|
||||
CleanUpDuplicates( mod );
|
||||
ClearEmptySubDirectories( dedup._baseDir );
|
||||
}
|
||||
|
||||
private void ReplaceFile( FileInfo f1, FileInfo f2 )
|
||||
{
|
||||
if( !Utf8RelPath.FromFile( f1, _baseDir, out var relName1 )
|
||||
|| !Utf8RelPath.FromFile( f2, _baseDir, out var relName2 ) )
|
||||
{
|
||||
Both = 0,
|
||||
Single = 1,
|
||||
Multi = 2,
|
||||
return;
|
||||
}
|
||||
|
||||
var inOption1 = false;
|
||||
var inOption2 = false;
|
||||
foreach( var option in _mod.Groups.SelectMany( g => g.Value.Options ) )
|
||||
{
|
||||
if( option.OptionFiles.ContainsKey( relName1 ) )
|
||||
{
|
||||
inOption1 = true;
|
||||
}
|
||||
|
||||
if( !option.OptionFiles.TryGetValue( relName2, out var values ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
inOption2 = true;
|
||||
|
||||
foreach( var value in values )
|
||||
{
|
||||
option.AddFile( relName1, value );
|
||||
}
|
||||
|
||||
option.OptionFiles.Remove( relName2 );
|
||||
}
|
||||
|
||||
if( !inOption1 || !inOption2 )
|
||||
{
|
||||
var duplicates = FindOrCreateDuplicates( _mod );
|
||||
if( !inOption1 )
|
||||
{
|
||||
duplicates.AddFile( relName1, relName2.ToGamePath() );
|
||||
}
|
||||
|
||||
if( !inOption2 )
|
||||
{
|
||||
duplicates.AddFile( relName1, relName1.ToGamePath() );
|
||||
}
|
||||
}
|
||||
|
||||
PluginLog.Information( $"File {relName1} and {relName2} are identical. Deleting the second." );
|
||||
f2.Delete();
|
||||
}
|
||||
|
||||
public static bool CompareFilesDirectly( FileInfo f1, FileInfo f2 )
|
||||
=> File.ReadAllBytes( f1.FullName ).SequenceEqual( File.ReadAllBytes( f2.FullName ) );
|
||||
|
||||
public static bool CompareHashes( byte[] f1, byte[] f2 )
|
||||
=> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool FileIsInAnyGroup( ModMeta meta, Utf8RelPath relPath, bool exceptDuplicates = false )
|
||||
{
|
||||
var groupEnumerator = exceptDuplicates
|
||||
? meta.Groups.Values.Where( g => g.GroupName != Duplicates )
|
||||
: meta.Groups.Values;
|
||||
return groupEnumerator.SelectMany( group => group.Options )
|
||||
.Any( option => option.OptionFiles.ContainsKey( relPath ) );
|
||||
}
|
||||
|
||||
private static void CleanUpDuplicates( ModMeta meta )
|
||||
{
|
||||
if( !meta.Groups.TryGetValue( Duplicates, out var info ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var requiredIdx = info.Options.FindIndex( o => o.OptionName == Required );
|
||||
if( requiredIdx >= 0 )
|
||||
{
|
||||
var required = info.Options[ requiredIdx ];
|
||||
foreach( var (key, value) in required.OptionFiles.ToArray() )
|
||||
{
|
||||
if( value.Count > 1 || FileIsInAnyGroup( meta, key, true ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if( value.Count == 0 || value.First().CompareTo( key.ToGamePath() ) == 0 )
|
||||
{
|
||||
required.OptionFiles.Remove( key );
|
||||
}
|
||||
}
|
||||
|
||||
if( required.OptionFiles.Count == 0 )
|
||||
{
|
||||
info.Options.RemoveAt( requiredIdx );
|
||||
}
|
||||
}
|
||||
|
||||
if( info.Options.Count == 0 )
|
||||
{
|
||||
meta.Groups.Remove( Duplicates );
|
||||
}
|
||||
}
|
||||
|
||||
public enum GroupType
|
||||
{
|
||||
Both = 0,
|
||||
Single = 1,
|
||||
Multi = 2,
|
||||
};
|
||||
|
||||
private static void RemoveFromGroups( ModMeta meta, Utf8RelPath relPath, Utf8GamePath gamePath, GroupType type = GroupType.Both,
|
||||
bool skipDuplicates = true )
|
||||
{
|
||||
if( meta.Groups.Count == 0 )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var enumerator = type switch
|
||||
{
|
||||
GroupType.Both => meta.Groups.Values,
|
||||
GroupType.Single => meta.Groups.Values.Where( g => g.SelectionType == SelectType.Single ),
|
||||
GroupType.Multi => meta.Groups.Values.Where( g => g.SelectionType == SelectType.Multi ),
|
||||
_ => throw new InvalidEnumArgumentException( "Invalid Enum in RemoveFromGroups" ),
|
||||
};
|
||||
|
||||
private static void RemoveFromGroups( ModMeta meta, RelPath relPath, GamePath gamePath, GroupType type = GroupType.Both,
|
||||
bool skipDuplicates = true )
|
||||
foreach( var group in enumerator )
|
||||
{
|
||||
if( meta.Groups.Count == 0 )
|
||||
var optionEnum = skipDuplicates
|
||||
? group.Options.Where( o => group.GroupName != Duplicates || o.OptionName != Required )
|
||||
: group.Options;
|
||||
foreach( var option in optionEnum )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var enumerator = type switch
|
||||
{
|
||||
GroupType.Both => meta.Groups.Values,
|
||||
GroupType.Single => meta.Groups.Values.Where( g => g.SelectionType == SelectType.Single ),
|
||||
GroupType.Multi => meta.Groups.Values.Where( g => g.SelectionType == SelectType.Multi ),
|
||||
_ => throw new InvalidEnumArgumentException( "Invalid Enum in RemoveFromGroups" ),
|
||||
};
|
||||
foreach( var group in enumerator )
|
||||
{
|
||||
var optionEnum = skipDuplicates
|
||||
? group.Options.Where( o => group.GroupName != Duplicates || o.OptionName != Required )
|
||||
: group.Options;
|
||||
foreach( var option in optionEnum )
|
||||
if( option.OptionFiles.TryGetValue( relPath, out var gamePaths ) && gamePaths.Remove( gamePath ) && gamePaths.Count == 0 )
|
||||
{
|
||||
if( option.OptionFiles.TryGetValue( relPath, out var gamePaths ) && gamePaths.Remove( gamePath ) && gamePaths.Count == 0 )
|
||||
{
|
||||
option.OptionFiles.Remove( relPath );
|
||||
}
|
||||
option.OptionFiles.Remove( relPath );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static bool MoveFile( ModMeta meta, string basePath, RelPath oldRelPath, RelPath newRelPath )
|
||||
public static bool MoveFile( ModMeta meta, string basePath, Utf8RelPath oldRelPath, Utf8RelPath newRelPath )
|
||||
{
|
||||
if( oldRelPath.Equals( newRelPath ) )
|
||||
{
|
||||
if( oldRelPath == newRelPath )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var newFullPath = Path.Combine( basePath, newRelPath );
|
||||
new FileInfo( newFullPath ).Directory!.Create();
|
||||
File.Move( Path.Combine( basePath, oldRelPath ), newFullPath );
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
PluginLog.Error( $"Could not move file from {oldRelPath} to {newRelPath}:\n{e}" );
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach( var option in meta.Groups.Values.SelectMany( group => group.Options ) )
|
||||
{
|
||||
if( option.OptionFiles.TryGetValue( oldRelPath, out var gamePaths ) )
|
||||
{
|
||||
option.OptionFiles.Add( newRelPath, gamePaths );
|
||||
option.OptionFiles.Remove( oldRelPath );
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private static void RemoveUselessGroups( ModMeta meta )
|
||||
try
|
||||
{
|
||||
meta.Groups = meta.Groups.Where( kvp => kvp.Value.Options.Any( o => o.OptionFiles.Count > 0 ) )
|
||||
.ToDictionary( kvp => kvp.Key, kvp => kvp.Value );
|
||||
var newFullPath = Path.Combine( basePath, newRelPath.ToString() );
|
||||
new FileInfo( newFullPath ).Directory!.Create();
|
||||
File.Move( Path.Combine( basePath, oldRelPath.ToString() ), newFullPath );
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
PluginLog.Error( $"Could not move file from {oldRelPath} to {newRelPath}:\n{e}" );
|
||||
return false;
|
||||
}
|
||||
|
||||
// Goes through all Single-Select options and checks if file links are in each of them.
|
||||
// If they are, it moves those files to the root folder and removes them from the groups (and puts them to duplicates, if necessary).
|
||||
public static void Normalize( DirectoryInfo baseDir, ModMeta meta )
|
||||
foreach( var option in meta.Groups.Values.SelectMany( group => group.Options ) )
|
||||
{
|
||||
foreach( var group in meta.Groups.Values.Where( g => g.SelectionType == SelectType.Single && g.GroupName != Duplicates ) )
|
||||
if( option.OptionFiles.TryGetValue( oldRelPath, out var gamePaths ) )
|
||||
{
|
||||
var firstOption = true;
|
||||
HashSet< (RelPath, GamePath) > groupList = new();
|
||||
foreach( var option in group.Options )
|
||||
option.OptionFiles.Add( newRelPath, gamePaths );
|
||||
option.OptionFiles.Remove( oldRelPath );
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private static void RemoveUselessGroups( ModMeta meta )
|
||||
{
|
||||
meta.Groups = meta.Groups.Where( kvp => kvp.Value.Options.Any( o => o.OptionFiles.Count > 0 ) )
|
||||
.ToDictionary( kvp => kvp.Key, kvp => kvp.Value );
|
||||
}
|
||||
|
||||
// Goes through all Single-Select options and checks if file links are in each of them.
|
||||
// If they are, it moves those files to the root folder and removes them from the groups (and puts them to duplicates, if necessary).
|
||||
public static void Normalize( DirectoryInfo baseDir, ModMeta meta )
|
||||
{
|
||||
foreach( var group in meta.Groups.Values.Where( g => g.SelectionType == SelectType.Single && g.GroupName != Duplicates ) )
|
||||
{
|
||||
var firstOption = true;
|
||||
HashSet< (Utf8RelPath, Utf8GamePath) > groupList = new();
|
||||
foreach( var option in group.Options )
|
||||
{
|
||||
HashSet< (Utf8RelPath, Utf8GamePath) > optionList = new();
|
||||
foreach( var (file, gamePaths) in option.OptionFiles.Select( p => ( p.Key, p.Value ) ) )
|
||||
{
|
||||
HashSet< (RelPath, GamePath) > optionList = new();
|
||||
foreach( var (file, gamePaths) in option.OptionFiles.Select( p => ( p.Key, p.Value ) ) )
|
||||
{
|
||||
optionList.UnionWith( gamePaths.Select( p => ( file, p ) ) );
|
||||
}
|
||||
|
||||
if( firstOption )
|
||||
{
|
||||
groupList = optionList;
|
||||
}
|
||||
else
|
||||
{
|
||||
groupList.IntersectWith( optionList );
|
||||
}
|
||||
|
||||
firstOption = false;
|
||||
optionList.UnionWith( gamePaths.Select( p => ( file, p ) ) );
|
||||
}
|
||||
|
||||
var newPath = new Dictionary< RelPath, GamePath >();
|
||||
foreach( var (path, gamePath) in groupList )
|
||||
if( firstOption )
|
||||
{
|
||||
var relPath = new RelPath( gamePath );
|
||||
if( newPath.TryGetValue( path, out var usedGamePath ) )
|
||||
{
|
||||
var required = FindOrCreateDuplicates( meta );
|
||||
var usedRelPath = new RelPath( usedGamePath );
|
||||
required.AddFile( usedRelPath, gamePath );
|
||||
required.AddFile( usedRelPath, usedGamePath );
|
||||
RemoveFromGroups( meta, relPath, gamePath, GroupType.Single );
|
||||
}
|
||||
else if( MoveFile( meta, baseDir.FullName, path, relPath ) )
|
||||
{
|
||||
newPath[ path ] = gamePath;
|
||||
if( FileIsInAnyGroup( meta, relPath ) )
|
||||
{
|
||||
FindOrCreateDuplicates( meta ).AddFile( relPath, gamePath );
|
||||
}
|
||||
|
||||
RemoveFromGroups( meta, relPath, gamePath, GroupType.Single );
|
||||
}
|
||||
groupList = optionList;
|
||||
}
|
||||
else
|
||||
{
|
||||
groupList.IntersectWith( optionList );
|
||||
}
|
||||
|
||||
firstOption = false;
|
||||
}
|
||||
|
||||
RemoveUselessGroups( meta );
|
||||
ClearEmptySubDirectories( baseDir );
|
||||
var newPath = new Dictionary< Utf8RelPath, Utf8GamePath >();
|
||||
foreach( var (path, gamePath) in groupList )
|
||||
{
|
||||
var relPath = new Utf8RelPath( gamePath );
|
||||
if( newPath.TryGetValue( path, out var usedGamePath ) )
|
||||
{
|
||||
var required = FindOrCreateDuplicates( meta );
|
||||
var usedRelPath = new Utf8RelPath( usedGamePath );
|
||||
required.AddFile( usedRelPath, gamePath );
|
||||
required.AddFile( usedRelPath, usedGamePath );
|
||||
RemoveFromGroups( meta, relPath, gamePath, GroupType.Single );
|
||||
}
|
||||
else if( MoveFile( meta, baseDir.FullName, path, relPath ) )
|
||||
{
|
||||
newPath[ path ] = gamePath;
|
||||
if( FileIsInAnyGroup( meta, relPath ) )
|
||||
{
|
||||
FindOrCreateDuplicates( meta ).AddFile( relPath, gamePath );
|
||||
}
|
||||
|
||||
RemoveFromGroups( meta, relPath, gamePath, GroupType.Single );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void AutoGenerateGroups( DirectoryInfo baseDir, ModMeta meta )
|
||||
RemoveUselessGroups( meta );
|
||||
ClearEmptySubDirectories( baseDir );
|
||||
}
|
||||
|
||||
public static void AutoGenerateGroups( DirectoryInfo baseDir, ModMeta meta )
|
||||
{
|
||||
meta.Groups.Clear();
|
||||
ClearEmptySubDirectories( baseDir );
|
||||
foreach( var groupDir in baseDir.EnumerateDirectories() )
|
||||
{
|
||||
meta.Groups.Clear();
|
||||
ClearEmptySubDirectories( baseDir );
|
||||
foreach( var groupDir in baseDir.EnumerateDirectories() )
|
||||
var group = new OptionGroup
|
||||
{
|
||||
var group = new OptionGroup
|
||||
GroupName = groupDir.Name,
|
||||
SelectionType = SelectType.Single,
|
||||
Options = new List< Option >(),
|
||||
};
|
||||
|
||||
foreach( var optionDir in groupDir.EnumerateDirectories() )
|
||||
{
|
||||
var option = new Option
|
||||
{
|
||||
GroupName = groupDir.Name,
|
||||
SelectionType = SelectType.Single,
|
||||
Options = new List< Option >(),
|
||||
OptionDesc = string.Empty,
|
||||
OptionName = optionDir.Name,
|
||||
OptionFiles = new Dictionary< Utf8RelPath, HashSet< Utf8GamePath > >(),
|
||||
};
|
||||
|
||||
foreach( var optionDir in groupDir.EnumerateDirectories() )
|
||||
foreach( var file in optionDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) )
|
||||
{
|
||||
var option = new Option
|
||||
if( Utf8RelPath.FromFile( file, baseDir, out var rel )
|
||||
&& Utf8GamePath.FromFile( file, optionDir, out var game ) )
|
||||
{
|
||||
OptionDesc = string.Empty,
|
||||
OptionName = optionDir.Name,
|
||||
OptionFiles = new Dictionary< RelPath, HashSet< GamePath > >(),
|
||||
};
|
||||
foreach( var file in optionDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) )
|
||||
{
|
||||
var relPath = new RelPath( file, baseDir );
|
||||
var gamePath = new GamePath( file, optionDir );
|
||||
option.OptionFiles[ relPath ] = new HashSet< GamePath > { gamePath };
|
||||
}
|
||||
|
||||
if( option.OptionFiles.Any() )
|
||||
{
|
||||
group.Options.Add( option );
|
||||
option.OptionFiles[ rel ] = new HashSet< Utf8GamePath > { game };
|
||||
}
|
||||
}
|
||||
|
||||
if( group.Options.Any() )
|
||||
if( option.OptionFiles.Any() )
|
||||
{
|
||||
meta.Groups.Add( groupDir.Name, group );
|
||||
group.Options.Add( option );
|
||||
}
|
||||
}
|
||||
|
||||
foreach(var collection in Penumbra.ModManager.Collections.Collections.Values)
|
||||
collection.UpdateSetting(baseDir, meta, true);
|
||||
if( group.Options.Any() )
|
||||
{
|
||||
meta.Groups.Add( groupDir.Name, group );
|
||||
}
|
||||
}
|
||||
|
||||
foreach( var collection in Penumbra.ModManager.Collections.Collections.Values )
|
||||
{
|
||||
collection.UpdateSetting( baseDir, meta, true );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,134 +3,133 @@ using System.Collections.Generic;
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using Dalamud.Logging;
|
||||
using Penumbra.GameData.ByteString;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.Mod
|
||||
namespace Penumbra.Mod;
|
||||
|
||||
public struct SortOrder : IComparable< SortOrder >
|
||||
{
|
||||
public struct SortOrder : IComparable< SortOrder >
|
||||
public ModFolder ParentFolder { get; set; }
|
||||
|
||||
private string _sortOrderName;
|
||||
|
||||
public string SortOrderName
|
||||
{
|
||||
public ModFolder ParentFolder { get; set; }
|
||||
|
||||
private string _sortOrderName;
|
||||
|
||||
public string SortOrderName
|
||||
{
|
||||
get => _sortOrderName;
|
||||
set => _sortOrderName = value.Replace( '/', '\\' );
|
||||
}
|
||||
|
||||
public string SortOrderPath
|
||||
=> ParentFolder.FullName;
|
||||
|
||||
public string FullName
|
||||
{
|
||||
get
|
||||
{
|
||||
var path = SortOrderPath;
|
||||
return path.Any() ? $"{path}/{SortOrderName}" : SortOrderName;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public SortOrder( ModFolder parentFolder, string name )
|
||||
{
|
||||
ParentFolder = parentFolder;
|
||||
_sortOrderName = name.Replace( '/', '\\' );
|
||||
}
|
||||
|
||||
public string FullPath
|
||||
=> SortOrderPath.Any() ? $"{SortOrderPath}/{SortOrderName}" : SortOrderName;
|
||||
|
||||
public int CompareTo( SortOrder other )
|
||||
=> string.Compare( FullPath, other.FullPath, StringComparison.InvariantCultureIgnoreCase );
|
||||
get => _sortOrderName;
|
||||
set => _sortOrderName = value.Replace( '/', '\\' );
|
||||
}
|
||||
|
||||
// ModData contains all permanent information about a mod,
|
||||
// and is independent of collections or settings.
|
||||
// It only changes when the user actively changes the mod or their filesystem.
|
||||
public class ModData
|
||||
public string SortOrderPath
|
||||
=> ParentFolder.FullName;
|
||||
|
||||
public string FullName
|
||||
{
|
||||
public DirectoryInfo BasePath;
|
||||
public ModMeta Meta;
|
||||
public ModResources Resources;
|
||||
|
||||
public SortOrder SortOrder;
|
||||
|
||||
public SortedList< string, object? > ChangedItems { get; } = new();
|
||||
public string LowerChangedItemsString { get; private set; } = string.Empty;
|
||||
public FileInfo MetaFile { get; set; }
|
||||
|
||||
private ModData( ModFolder parentFolder, DirectoryInfo basePath, ModMeta meta, ModResources resources )
|
||||
get
|
||||
{
|
||||
BasePath = basePath;
|
||||
Meta = meta;
|
||||
Resources = resources;
|
||||
MetaFile = MetaFileInfo( basePath );
|
||||
SortOrder = new SortOrder( parentFolder, Meta.Name );
|
||||
SortOrder.ParentFolder.AddMod( this );
|
||||
|
||||
ComputeChangedItems();
|
||||
var path = SortOrderPath;
|
||||
return path.Length > 0 ? $"{path}/{SortOrderName}" : SortOrderName;
|
||||
}
|
||||
|
||||
public void ComputeChangedItems()
|
||||
{
|
||||
var identifier = GameData.GameData.GetIdentifier();
|
||||
ChangedItems.Clear();
|
||||
foreach( var file in Resources.ModFiles.Select( f => new RelPath( f, BasePath ) ) )
|
||||
{
|
||||
foreach( var path in ModFunctions.GetAllFiles( file, Meta ) )
|
||||
{
|
||||
identifier.Identify( ChangedItems, path );
|
||||
}
|
||||
}
|
||||
|
||||
foreach( var path in Meta.FileSwaps.Keys )
|
||||
{
|
||||
identifier.Identify( ChangedItems, path );
|
||||
}
|
||||
|
||||
LowerChangedItemsString = string.Join( "\0", ChangedItems.Keys.Select( k => k.ToLowerInvariant() ) );
|
||||
}
|
||||
|
||||
public static FileInfo MetaFileInfo( DirectoryInfo basePath )
|
||||
=> new( Path.Combine( basePath.FullName, "meta.json" ) );
|
||||
|
||||
public static ModData? LoadMod( ModFolder parentFolder, DirectoryInfo basePath )
|
||||
{
|
||||
basePath.Refresh();
|
||||
if( !basePath.Exists )
|
||||
{
|
||||
PluginLog.Error( $"Supplied mod directory {basePath} does not exist." );
|
||||
return null;
|
||||
}
|
||||
|
||||
var metaFile = MetaFileInfo( basePath );
|
||||
if( !metaFile.Exists )
|
||||
{
|
||||
PluginLog.Debug( "No mod meta found for {ModLocation}.", basePath.Name );
|
||||
return null;
|
||||
}
|
||||
|
||||
var meta = ModMeta.LoadFromFile( metaFile );
|
||||
if( meta == null )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var data = new ModResources();
|
||||
if( data.RefreshModFiles( basePath ).HasFlag( ResourceChange.Meta ) )
|
||||
{
|
||||
data.SetManipulations( meta, basePath );
|
||||
}
|
||||
|
||||
return new ModData( parentFolder, basePath, meta, data );
|
||||
}
|
||||
|
||||
public void SaveMeta()
|
||||
=> Meta.SaveToFile( MetaFile );
|
||||
|
||||
public override string ToString()
|
||||
=> SortOrder.FullPath;
|
||||
}
|
||||
|
||||
|
||||
public SortOrder( ModFolder parentFolder, string name )
|
||||
{
|
||||
ParentFolder = parentFolder;
|
||||
_sortOrderName = name.Replace( '/', '\\' );
|
||||
}
|
||||
|
||||
public string FullPath
|
||||
=> SortOrderPath.Length > 0 ? $"{SortOrderPath}/{SortOrderName}" : SortOrderName;
|
||||
|
||||
public int CompareTo( SortOrder other )
|
||||
=> string.Compare( FullPath, other.FullPath, StringComparison.InvariantCultureIgnoreCase );
|
||||
}
|
||||
|
||||
// ModData contains all permanent information about a mod,
|
||||
// and is independent of collections or settings.
|
||||
// It only changes when the user actively changes the mod or their filesystem.
|
||||
public class ModData
|
||||
{
|
||||
public DirectoryInfo BasePath;
|
||||
public ModMeta Meta;
|
||||
public ModResources Resources;
|
||||
|
||||
public SortOrder SortOrder;
|
||||
|
||||
public SortedList< string, object? > ChangedItems { get; } = new();
|
||||
public string LowerChangedItemsString { get; private set; } = string.Empty;
|
||||
public FileInfo MetaFile { get; set; }
|
||||
|
||||
private ModData( ModFolder parentFolder, DirectoryInfo basePath, ModMeta meta, ModResources resources )
|
||||
{
|
||||
BasePath = basePath;
|
||||
Meta = meta;
|
||||
Resources = resources;
|
||||
MetaFile = MetaFileInfo( basePath );
|
||||
SortOrder = new SortOrder( parentFolder, Meta.Name );
|
||||
SortOrder.ParentFolder.AddMod( this );
|
||||
|
||||
ComputeChangedItems();
|
||||
}
|
||||
|
||||
public void ComputeChangedItems()
|
||||
{
|
||||
var identifier = GameData.GameData.GetIdentifier();
|
||||
ChangedItems.Clear();
|
||||
foreach( var file in Resources.ModFiles.Select( f => f.ToRelPath( BasePath, out var p ) ? p : Utf8RelPath.Empty ) )
|
||||
{
|
||||
foreach( var path in ModFunctions.GetAllFiles( file, Meta ) )
|
||||
{
|
||||
identifier.Identify( ChangedItems, path.ToGamePath() );
|
||||
}
|
||||
}
|
||||
|
||||
foreach( var path in Meta.FileSwaps.Keys )
|
||||
{
|
||||
identifier.Identify( ChangedItems, path.ToGamePath() );
|
||||
}
|
||||
|
||||
LowerChangedItemsString = string.Join( "\0", ChangedItems.Keys.Select( k => k.ToLowerInvariant() ) );
|
||||
}
|
||||
|
||||
public static FileInfo MetaFileInfo( DirectoryInfo basePath )
|
||||
=> new(Path.Combine( basePath.FullName, "meta.json" ));
|
||||
|
||||
public static ModData? LoadMod( ModFolder parentFolder, DirectoryInfo basePath )
|
||||
{
|
||||
basePath.Refresh();
|
||||
if( !basePath.Exists )
|
||||
{
|
||||
PluginLog.Error( $"Supplied mod directory {basePath} does not exist." );
|
||||
return null;
|
||||
}
|
||||
|
||||
var metaFile = MetaFileInfo( basePath );
|
||||
if( !metaFile.Exists )
|
||||
{
|
||||
PluginLog.Debug( "No mod meta found for {ModLocation}.", basePath.Name );
|
||||
return null;
|
||||
}
|
||||
|
||||
var meta = ModMeta.LoadFromFile( metaFile );
|
||||
if( meta == null )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var data = new ModResources();
|
||||
if( data.RefreshModFiles( basePath ).HasFlag( ResourceChange.Meta ) )
|
||||
{
|
||||
data.SetManipulations( meta, basePath );
|
||||
}
|
||||
|
||||
return new ModData( parentFolder, basePath, meta, data );
|
||||
}
|
||||
|
||||
public void SaveMeta()
|
||||
=> Meta.SaveToFile( MetaFile );
|
||||
|
||||
public override string ToString()
|
||||
=> SortOrder.FullPath;
|
||||
}
|
||||
|
|
@ -1,103 +1,100 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Penumbra.GameData.Util;
|
||||
using Penumbra.Structs;
|
||||
using Penumbra.Util;
|
||||
using Penumbra.GameData.ByteString;
|
||||
|
||||
namespace Penumbra.Mod
|
||||
namespace Penumbra.Mod;
|
||||
|
||||
// Functions that do not really depend on only one component of a mod.
|
||||
public static class ModFunctions
|
||||
{
|
||||
// Functions that do not really depend on only one component of a mod.
|
||||
public static class ModFunctions
|
||||
public static bool CleanUpCollection( Dictionary< string, ModSettings > settings, IEnumerable< DirectoryInfo > modPaths )
|
||||
{
|
||||
public static bool CleanUpCollection( Dictionary< string, ModSettings > settings, IEnumerable< DirectoryInfo > modPaths )
|
||||
var hashes = modPaths.Select( p => p.Name ).ToHashSet();
|
||||
var missingMods = settings.Keys.Where( k => !hashes.Contains( k ) ).ToArray();
|
||||
var anyChanges = false;
|
||||
foreach( var toRemove in missingMods )
|
||||
{
|
||||
var hashes = modPaths.Select( p => p.Name ).ToHashSet();
|
||||
var missingMods = settings.Keys.Where( k => !hashes.Contains( k ) ).ToArray();
|
||||
var anyChanges = false;
|
||||
foreach( var toRemove in missingMods )
|
||||
{
|
||||
anyChanges |= settings.Remove( toRemove );
|
||||
}
|
||||
|
||||
return anyChanges;
|
||||
anyChanges |= settings.Remove( toRemove );
|
||||
}
|
||||
|
||||
public static HashSet< GamePath > GetFilesForConfig( RelPath relPath, ModSettings settings, ModMeta meta )
|
||||
return anyChanges;
|
||||
}
|
||||
|
||||
public static HashSet< Utf8GamePath > GetFilesForConfig( Utf8RelPath relPath, ModSettings settings, ModMeta meta )
|
||||
{
|
||||
var doNotAdd = false;
|
||||
var files = new HashSet< Utf8GamePath >();
|
||||
foreach( var group in meta.Groups.Values.Where( g => g.Options.Count > 0 ) )
|
||||
{
|
||||
var doNotAdd = false;
|
||||
var files = new HashSet< GamePath >();
|
||||
foreach( var group in meta.Groups.Values.Where( g => g.Options.Count > 0 ) )
|
||||
{
|
||||
doNotAdd |= group.ApplyGroupFiles( relPath, settings.Settings[ group.GroupName ], files );
|
||||
}
|
||||
|
||||
if( !doNotAdd )
|
||||
{
|
||||
files.Add( new GamePath( relPath ) );
|
||||
}
|
||||
|
||||
return files;
|
||||
doNotAdd |= group.ApplyGroupFiles( relPath, settings.Settings[ group.GroupName ], files );
|
||||
}
|
||||
|
||||
public static HashSet< GamePath > GetAllFiles( RelPath relPath, ModMeta meta )
|
||||
if( !doNotAdd )
|
||||
{
|
||||
var ret = new HashSet< GamePath >();
|
||||
foreach( var option in meta.Groups.Values.SelectMany( g => g.Options ) )
|
||||
{
|
||||
if( option.OptionFiles.TryGetValue( relPath, out var files ) )
|
||||
{
|
||||
ret.UnionWith( files );
|
||||
}
|
||||
}
|
||||
|
||||
if( ret.Count == 0 )
|
||||
{
|
||||
ret.Add( relPath.ToGamePath() );
|
||||
}
|
||||
|
||||
return ret;
|
||||
files.Add( relPath.ToGamePath() );
|
||||
}
|
||||
|
||||
public static ModSettings ConvertNamedSettings( NamedModSettings namedSettings, ModMeta meta )
|
||||
return files;
|
||||
}
|
||||
|
||||
public static HashSet< Utf8GamePath > GetAllFiles( Utf8RelPath relPath, ModMeta meta )
|
||||
{
|
||||
var ret = new HashSet< Utf8GamePath >();
|
||||
foreach( var option in meta.Groups.Values.SelectMany( g => g.Options ) )
|
||||
{
|
||||
ModSettings ret = new()
|
||||
if( option.OptionFiles.TryGetValue( relPath, out var files ) )
|
||||
{
|
||||
Priority = namedSettings.Priority,
|
||||
Settings = namedSettings.Settings.Keys.ToDictionary( k => k, _ => 0 ),
|
||||
};
|
||||
ret.UnionWith( files );
|
||||
}
|
||||
}
|
||||
|
||||
foreach( var kvp in namedSettings.Settings )
|
||||
if( ret.Count == 0 )
|
||||
{
|
||||
ret.Add( relPath.ToGamePath() );
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static ModSettings ConvertNamedSettings( NamedModSettings namedSettings, ModMeta meta )
|
||||
{
|
||||
ModSettings ret = new()
|
||||
{
|
||||
Priority = namedSettings.Priority,
|
||||
Settings = namedSettings.Settings.Keys.ToDictionary( k => k, _ => 0 ),
|
||||
};
|
||||
|
||||
foreach( var setting in namedSettings.Settings.Keys )
|
||||
{
|
||||
if( !meta.Groups.TryGetValue( setting, out var info ) )
|
||||
{
|
||||
if( !meta.Groups.TryGetValue( kvp.Key, out var info ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if( info.SelectionType == SelectType.Single )
|
||||
if( info.SelectionType == SelectType.Single )
|
||||
{
|
||||
if( namedSettings.Settings[ setting ].Count == 0 )
|
||||
{
|
||||
if( namedSettings.Settings[ kvp.Key ].Count == 0 )
|
||||
{
|
||||
ret.Settings[ kvp.Key ] = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
var idx = info.Options.FindIndex( o => o.OptionName == namedSettings.Settings[ kvp.Key ].Last() );
|
||||
ret.Settings[ kvp.Key ] = idx < 0 ? 0 : idx;
|
||||
}
|
||||
ret.Settings[ setting ] = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach( var idx in namedSettings.Settings[ kvp.Key ]
|
||||
.Select( option => info.Options.FindIndex( o => o.OptionName == option ) )
|
||||
.Where( idx => idx >= 0 ) )
|
||||
{
|
||||
ret.Settings[ kvp.Key ] |= 1 << idx;
|
||||
}
|
||||
var idx = info.Options.FindIndex( o => o.OptionName == namedSettings.Settings[ setting ].Last() );
|
||||
ret.Settings[ setting ] = idx < 0 ? 0 : idx;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach( var idx in namedSettings.Settings[ setting ]
|
||||
.Select( option => info.Options.FindIndex( o => o.OptionName == option ) )
|
||||
.Where( idx => idx >= 0 ) )
|
||||
{
|
||||
ret.Settings[ setting ] |= 1 << idx;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
|
@ -4,134 +4,133 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using Dalamud.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Penumbra.GameData.ByteString;
|
||||
using Penumbra.GameData.Util;
|
||||
using Penumbra.Structs;
|
||||
|
||||
namespace Penumbra.Mod
|
||||
namespace Penumbra.Mod;
|
||||
|
||||
// Contains descriptive data about the mod as well as possible settings and fileswaps.
|
||||
public class ModMeta
|
||||
{
|
||||
// Contains descriptive data about the mod as well as possible settings and fileswaps.
|
||||
public class ModMeta
|
||||
public uint FileVersion { get; set; }
|
||||
|
||||
public string Name
|
||||
{
|
||||
public uint FileVersion { get; set; }
|
||||
|
||||
public string Name
|
||||
get => _name;
|
||||
set
|
||||
{
|
||||
get => _name;
|
||||
set
|
||||
{
|
||||
_name = value;
|
||||
LowerName = value.ToLowerInvariant();
|
||||
}
|
||||
_name = value;
|
||||
LowerName = value.ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
private string _name = "Mod";
|
||||
private string _name = "Mod";
|
||||
|
||||
[JsonIgnore]
|
||||
public string LowerName { get; private set; } = "mod";
|
||||
[JsonIgnore]
|
||||
public string LowerName { get; private set; } = "mod";
|
||||
|
||||
private string _author = "";
|
||||
private string _author = "";
|
||||
|
||||
public string Author
|
||||
public string Author
|
||||
{
|
||||
get => _author;
|
||||
set
|
||||
{
|
||||
get => _author;
|
||||
set
|
||||
{
|
||||
_author = value;
|
||||
LowerAuthor = value.ToLowerInvariant();
|
||||
}
|
||||
_author = value;
|
||||
LowerAuthor = value.ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public string LowerAuthor { get; private set; } = "";
|
||||
[JsonIgnore]
|
||||
public string LowerAuthor { get; private set; } = "";
|
||||
|
||||
public string Description { get; set; } = "";
|
||||
public string Version { get; set; } = "";
|
||||
public string Website { get; set; } = "";
|
||||
public string Description { get; set; } = "";
|
||||
public string Version { get; set; } = "";
|
||||
public string Website { get; set; } = "";
|
||||
|
||||
[JsonProperty( ItemConverterType = typeof( GamePathConverter ) )]
|
||||
public Dictionary< GamePath, GamePath > FileSwaps { get; set; } = new();
|
||||
[JsonProperty( ItemConverterType = typeof( FullPath.FullPathConverter ) )]
|
||||
public Dictionary< Utf8GamePath, FullPath > FileSwaps { get; set; } = new();
|
||||
|
||||
public Dictionary< string, OptionGroup > Groups { get; set; } = new();
|
||||
public Dictionary< string, OptionGroup > Groups { get; set; } = new();
|
||||
|
||||
[JsonIgnore]
|
||||
private int FileHash { get; set; }
|
||||
[JsonIgnore]
|
||||
private int FileHash { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public bool HasGroupsWithConfig { get; private set; }
|
||||
[JsonIgnore]
|
||||
public bool HasGroupsWithConfig { get; private set; }
|
||||
|
||||
public bool RefreshFromFile( FileInfo filePath )
|
||||
public bool RefreshFromFile( FileInfo filePath )
|
||||
{
|
||||
var newMeta = LoadFromFile( filePath );
|
||||
if( newMeta == null )
|
||||
{
|
||||
var newMeta = LoadFromFile( filePath );
|
||||
if( newMeta == null )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if( newMeta.FileHash == FileHash )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
FileVersion = newMeta.FileVersion;
|
||||
Name = newMeta.Name;
|
||||
Author = newMeta.Author;
|
||||
Description = newMeta.Description;
|
||||
Version = newMeta.Version;
|
||||
Website = newMeta.Website;
|
||||
FileSwaps = newMeta.FileSwaps;
|
||||
Groups = newMeta.Groups;
|
||||
FileHash = newMeta.FileHash;
|
||||
HasGroupsWithConfig = newMeta.HasGroupsWithConfig;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static ModMeta? LoadFromFile( FileInfo filePath )
|
||||
if( newMeta.FileHash == FileHash )
|
||||
{
|
||||
try
|
||||
{
|
||||
var text = File.ReadAllText( filePath.FullName );
|
||||
|
||||
var meta = JsonConvert.DeserializeObject< ModMeta >( text,
|
||||
new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore } );
|
||||
if( meta != null )
|
||||
{
|
||||
meta.FileHash = text.GetHashCode();
|
||||
meta.RefreshHasGroupsWithConfig();
|
||||
}
|
||||
|
||||
return meta;
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
PluginLog.Error( $"Could not load mod meta:\n{e}" );
|
||||
return null;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool RefreshHasGroupsWithConfig()
|
||||
FileVersion = newMeta.FileVersion;
|
||||
Name = newMeta.Name;
|
||||
Author = newMeta.Author;
|
||||
Description = newMeta.Description;
|
||||
Version = newMeta.Version;
|
||||
Website = newMeta.Website;
|
||||
FileSwaps = newMeta.FileSwaps;
|
||||
Groups = newMeta.Groups;
|
||||
FileHash = newMeta.FileHash;
|
||||
HasGroupsWithConfig = newMeta.HasGroupsWithConfig;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static ModMeta? LoadFromFile( FileInfo filePath )
|
||||
{
|
||||
try
|
||||
{
|
||||
var oldValue = HasGroupsWithConfig;
|
||||
HasGroupsWithConfig = Groups.Values.Any( g => g.Options.Count > 1 || g.SelectionType == SelectType.Multi && g.Options.Count == 1 );
|
||||
return oldValue != HasGroupsWithConfig;
|
||||
var text = File.ReadAllText( filePath.FullName );
|
||||
|
||||
var meta = JsonConvert.DeserializeObject< ModMeta >( text,
|
||||
new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore } );
|
||||
if( meta != null )
|
||||
{
|
||||
meta.FileHash = text.GetHashCode();
|
||||
meta.RefreshHasGroupsWithConfig();
|
||||
}
|
||||
|
||||
return meta;
|
||||
}
|
||||
|
||||
|
||||
public void SaveToFile( FileInfo filePath )
|
||||
catch( Exception e )
|
||||
{
|
||||
try
|
||||
PluginLog.Error( $"Could not load mod meta:\n{e}" );
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public bool RefreshHasGroupsWithConfig()
|
||||
{
|
||||
var oldValue = HasGroupsWithConfig;
|
||||
HasGroupsWithConfig = Groups.Values.Any( g => g.Options.Count > 1 || g.SelectionType == SelectType.Multi && g.Options.Count == 1 );
|
||||
return oldValue != HasGroupsWithConfig;
|
||||
}
|
||||
|
||||
|
||||
public void SaveToFile( FileInfo filePath )
|
||||
{
|
||||
try
|
||||
{
|
||||
var text = JsonConvert.SerializeObject( this, Formatting.Indented );
|
||||
var newHash = text.GetHashCode();
|
||||
if( newHash != FileHash )
|
||||
{
|
||||
var text = JsonConvert.SerializeObject( this, Formatting.Indented );
|
||||
var newHash = text.GetHashCode();
|
||||
if( newHash != FileHash )
|
||||
{
|
||||
File.WriteAllText( filePath.FullName, text );
|
||||
FileHash = newHash;
|
||||
}
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
PluginLog.Error( $"Could not write meta file for mod {Name} to {filePath.FullName}:\n{e}" );
|
||||
File.WriteAllText( filePath.FullName, text );
|
||||
FileHash = newHash;
|
||||
}
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
PluginLog.Error( $"Could not write meta file for mod {Name} to {filePath.FullName}:\n{e}" );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,75 +1,73 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Penumbra.Structs;
|
||||
|
||||
namespace Penumbra.Mod
|
||||
namespace Penumbra.Mod;
|
||||
|
||||
// Contains the settings for a given mod.
|
||||
public class ModSettings
|
||||
{
|
||||
// Contains the settings for a given mod.
|
||||
public class ModSettings
|
||||
public bool Enabled { get; set; }
|
||||
public int Priority { get; set; }
|
||||
public Dictionary< string, int > Settings { get; set; } = new();
|
||||
|
||||
// For backwards compatibility
|
||||
private Dictionary< string, int > Conf
|
||||
{
|
||||
public bool Enabled { get; set; }
|
||||
public int Priority { get; set; }
|
||||
public Dictionary< string, int > Settings { get; set; } = new();
|
||||
set => Settings = value;
|
||||
}
|
||||
|
||||
// For backwards compatibility
|
||||
private Dictionary< string, int > Conf
|
||||
public ModSettings DeepCopy()
|
||||
{
|
||||
var settings = new ModSettings
|
||||
{
|
||||
set => Settings = value;
|
||||
Enabled = Enabled,
|
||||
Priority = Priority,
|
||||
Settings = Settings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value ),
|
||||
};
|
||||
return settings;
|
||||
}
|
||||
|
||||
public static ModSettings DefaultSettings( ModMeta meta )
|
||||
{
|
||||
return new ModSettings
|
||||
{
|
||||
Enabled = false,
|
||||
Priority = 0,
|
||||
Settings = meta.Groups.ToDictionary( kvp => kvp.Key, _ => 0 ),
|
||||
};
|
||||
}
|
||||
|
||||
public bool FixSpecificSetting( string name, ModMeta meta )
|
||||
{
|
||||
if( !meta.Groups.TryGetValue( name, out var group ) )
|
||||
{
|
||||
return Settings.Remove( name );
|
||||
}
|
||||
|
||||
public ModSettings DeepCopy()
|
||||
if( Settings.TryGetValue( name, out var oldSetting ) )
|
||||
{
|
||||
var settings = new ModSettings
|
||||
Settings[ name ] = group.SelectionType switch
|
||||
{
|
||||
Enabled = Enabled,
|
||||
Priority = Priority,
|
||||
Settings = Settings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value ),
|
||||
SelectType.Single => Math.Min( Math.Max( oldSetting, 0 ), group.Options.Count - 1 ),
|
||||
SelectType.Multi => Math.Min( Math.Max( oldSetting, 0 ), ( 1 << group.Options.Count ) - 1 ),
|
||||
_ => Settings[ group.GroupName ],
|
||||
};
|
||||
return settings;
|
||||
return oldSetting != Settings[ group.GroupName ];
|
||||
}
|
||||
|
||||
public static ModSettings DefaultSettings( ModMeta meta )
|
||||
Settings[ name ] = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool FixInvalidSettings( ModMeta meta )
|
||||
{
|
||||
if( meta.Groups.Count == 0 )
|
||||
{
|
||||
return new()
|
||||
{
|
||||
Enabled = false,
|
||||
Priority = 0,
|
||||
Settings = meta.Groups.ToDictionary( kvp => kvp.Key, _ => 0 ),
|
||||
};
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool FixSpecificSetting( string name, ModMeta meta )
|
||||
{
|
||||
if( !meta.Groups.TryGetValue( name, out var group ) )
|
||||
{
|
||||
return Settings.Remove( name );
|
||||
}
|
||||
|
||||
if( Settings.TryGetValue( name, out var oldSetting ) )
|
||||
{
|
||||
Settings[ name ] = group.SelectionType switch
|
||||
{
|
||||
SelectType.Single => Math.Min( Math.Max( oldSetting, 0 ), group.Options.Count - 1 ),
|
||||
SelectType.Multi => Math.Min( Math.Max( oldSetting, 0 ), ( 1 << group.Options.Count ) - 1 ),
|
||||
_ => Settings[ group.GroupName ],
|
||||
};
|
||||
return oldSetting != Settings[ group.GroupName ];
|
||||
}
|
||||
|
||||
Settings[ name ] = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool FixInvalidSettings( ModMeta meta )
|
||||
{
|
||||
if( meta.Groups.Count == 0 )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return Settings.Keys.ToArray().Union( meta.Groups.Keys )
|
||||
.Aggregate( false, ( current, name ) => current | FixSpecificSetting( name, meta ) );
|
||||
}
|
||||
return Settings.Keys.ToArray().Union( meta.Groups.Keys )
|
||||
.Aggregate( false, ( current, name ) => current | FixSpecificSetting( name, meta ) );
|
||||
}
|
||||
}
|
||||
|
|
@ -1,45 +1,43 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Penumbra.Structs;
|
||||
|
||||
namespace Penumbra.Mod
|
||||
namespace Penumbra.Mod;
|
||||
|
||||
// Contains settings with the option selections stored by names instead of index.
|
||||
// This is meant to make them possibly more portable when we support importing collections from other users.
|
||||
// Enabled does not exist, because disabled mods would not be exported in this way.
|
||||
public class NamedModSettings
|
||||
{
|
||||
// Contains settings with the option selections stored by names instead of index.
|
||||
// This is meant to make them possibly more portable when we support importing collections from other users.
|
||||
// Enabled does not exist, because disabled mods would not be exported in this way.
|
||||
public class NamedModSettings
|
||||
public int Priority { get; set; }
|
||||
public Dictionary< string, HashSet< string > > Settings { get; set; } = new();
|
||||
|
||||
public void AddFromModSetting( ModSettings s, ModMeta meta )
|
||||
{
|
||||
public int Priority { get; set; }
|
||||
public Dictionary< string, HashSet< string > > Settings { get; set; } = new();
|
||||
Priority = s.Priority;
|
||||
Settings = s.Settings.Keys.ToDictionary( k => k, _ => new HashSet< string >() );
|
||||
|
||||
public void AddFromModSetting( ModSettings s, ModMeta meta )
|
||||
foreach( var kvp in Settings )
|
||||
{
|
||||
Priority = s.Priority;
|
||||
Settings = s.Settings.Keys.ToDictionary( k => k, _ => new HashSet< string >() );
|
||||
|
||||
foreach( var kvp in Settings )
|
||||
if( !meta.Groups.TryGetValue( kvp.Key, out var info ) )
|
||||
{
|
||||
if( !meta.Groups.TryGetValue( kvp.Key, out var info ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
var setting = s.Settings[ kvp.Key ];
|
||||
if( info.SelectionType == SelectType.Single )
|
||||
var setting = s.Settings[ kvp.Key ];
|
||||
if( info.SelectionType == SelectType.Single )
|
||||
{
|
||||
var name = setting < info.Options.Count
|
||||
? info.Options[ setting ].OptionName
|
||||
: info.Options[ 0 ].OptionName;
|
||||
kvp.Value.Add( name );
|
||||
}
|
||||
else
|
||||
{
|
||||
for( var i = 0; i < info.Options.Count; ++i )
|
||||
{
|
||||
var name = setting < info.Options.Count
|
||||
? info.Options[ setting ].OptionName
|
||||
: info.Options[ 0 ].OptionName;
|
||||
kvp.Value.Add( name );
|
||||
}
|
||||
else
|
||||
{
|
||||
for( var i = 0; i < info.Options.Count; ++i )
|
||||
if( ( ( setting >> i ) & 1 ) != 0 )
|
||||
{
|
||||
if( ( ( setting >> i ) & 1 ) != 0 )
|
||||
{
|
||||
kvp.Value.Add( info.Options[ i ].OptionName );
|
||||
}
|
||||
kvp.Value.Add( info.Options[ i ].OptionName );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ using System.Collections.Generic;
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using Dalamud.Logging;
|
||||
using Penumbra.Interop;
|
||||
using Penumbra.Mod;
|
||||
using Penumbra.Util;
|
||||
|
||||
|
|
@ -250,16 +249,16 @@ public class CollectionManager
|
|||
|
||||
public bool CreateCharacterCollection( string characterName )
|
||||
{
|
||||
if( !CharacterCollection.ContainsKey( characterName ) )
|
||||
if( CharacterCollection.ContainsKey( characterName ) )
|
||||
{
|
||||
CharacterCollection[ characterName ] = ModCollection.Empty;
|
||||
Penumbra.Config.CharacterCollections[ characterName ] = string.Empty;
|
||||
Penumbra.Config.Save();
|
||||
Penumbra.PlayerWatcher.AddPlayerToWatch( characterName );
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
CharacterCollection[ characterName ] = ModCollection.Empty;
|
||||
Penumbra.Config.CharacterCollections[ characterName ] = string.Empty;
|
||||
Penumbra.Config.Save();
|
||||
Penumbra.PlayerWatcher.AddPlayerToWatch( characterName );
|
||||
return true;
|
||||
}
|
||||
|
||||
public void RemoveCharacterCollection( string characterName )
|
||||
|
|
@ -299,7 +298,7 @@ public class CollectionManager
|
|||
|
||||
private bool LoadForcedCollection( Configuration config )
|
||||
{
|
||||
if( config.ForcedCollection == string.Empty )
|
||||
if( config.ForcedCollection.Length == 0 )
|
||||
{
|
||||
ForcedCollection = ModCollection.Empty;
|
||||
return false;
|
||||
|
|
@ -320,7 +319,7 @@ public class CollectionManager
|
|||
|
||||
private bool LoadDefaultCollection( Configuration config )
|
||||
{
|
||||
if( config.DefaultCollection == string.Empty )
|
||||
if( config.DefaultCollection.Length == 0 )
|
||||
{
|
||||
DefaultCollection = ModCollection.Empty;
|
||||
return false;
|
||||
|
|
@ -345,7 +344,7 @@ public class CollectionManager
|
|||
foreach( var (player, collectionName) in config.CharacterCollections.ToArray() )
|
||||
{
|
||||
Penumbra.PlayerWatcher.AddPlayerToWatch( player );
|
||||
if( collectionName == string.Empty )
|
||||
if( collectionName.Length == 0 )
|
||||
{
|
||||
CharacterCollection.Add( player, ModCollection.Empty );
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,255 +1,256 @@
|
|||
using Dalamud.Plugin;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Dalamud.Logging;
|
||||
using Penumbra.GameData.ByteString;
|
||||
using Penumbra.GameData.Util;
|
||||
using Penumbra.Interop;
|
||||
using Penumbra.Mod;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.Mods
|
||||
namespace Penumbra.Mods;
|
||||
|
||||
// A ModCollection is a named set of ModSettings to all of the users' installed mods.
|
||||
// It is meant to be local only, and thus should always contain settings for every mod, not just the enabled ones.
|
||||
// Settings to mods that are not installed anymore are kept as long as no call to CleanUnavailableSettings is made.
|
||||
// Active ModCollections build a cache of currently relevant data.
|
||||
public class ModCollection
|
||||
{
|
||||
// A ModCollection is a named set of ModSettings to all of the users' installed mods.
|
||||
// It is meant to be local only, and thus should always contain settings for every mod, not just the enabled ones.
|
||||
// Settings to mods that are not installed anymore are kept as long as no call to CleanUnavailableSettings is made.
|
||||
// Active ModCollections build a cache of currently relevant data.
|
||||
public class ModCollection
|
||||
public const string DefaultCollection = "Default";
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public Dictionary< string, ModSettings > Settings { get; }
|
||||
|
||||
public ModCollection()
|
||||
{
|
||||
public const string DefaultCollection = "Default";
|
||||
Name = DefaultCollection;
|
||||
Settings = new Dictionary< string, ModSettings >();
|
||||
}
|
||||
|
||||
public string Name { get; set; }
|
||||
public ModCollection( string name, Dictionary< string, ModSettings > settings )
|
||||
{
|
||||
Name = name;
|
||||
Settings = settings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value.DeepCopy() );
|
||||
}
|
||||
|
||||
public Dictionary< string, ModSettings > Settings { get; }
|
||||
|
||||
public ModCollection()
|
||||
public Mod.Mod GetMod( ModData mod )
|
||||
{
|
||||
if( Cache != null && Cache.AvailableMods.TryGetValue( mod.BasePath.Name, out var ret ) )
|
||||
{
|
||||
Name = DefaultCollection;
|
||||
Settings = new Dictionary< string, ModSettings >();
|
||||
return ret;
|
||||
}
|
||||
|
||||
public ModCollection( string name, Dictionary< string, ModSettings > settings )
|
||||
if( Settings.TryGetValue( mod.BasePath.Name, out var settings ) )
|
||||
{
|
||||
Name = name;
|
||||
Settings = settings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value.DeepCopy() );
|
||||
return new Mod.Mod( settings, mod );
|
||||
}
|
||||
|
||||
public Mod.Mod GetMod( ModData mod )
|
||||
{
|
||||
if( Cache != null && Cache.AvailableMods.TryGetValue( mod.BasePath.Name, out var ret ) )
|
||||
{
|
||||
return ret;
|
||||
}
|
||||
var newSettings = ModSettings.DefaultSettings( mod.Meta );
|
||||
Settings.Add( mod.BasePath.Name, newSettings );
|
||||
Save();
|
||||
return new Mod.Mod( newSettings, mod );
|
||||
}
|
||||
|
||||
private bool CleanUnavailableSettings( Dictionary< string, ModData > data )
|
||||
{
|
||||
var removeList = Settings.Where( settingKvp => !data.ContainsKey( settingKvp.Key ) ).ToArray();
|
||||
|
||||
foreach( var s in removeList )
|
||||
{
|
||||
Settings.Remove( s.Key );
|
||||
}
|
||||
|
||||
return removeList.Length > 0;
|
||||
}
|
||||
|
||||
public void CreateCache( DirectoryInfo modDirectory, IEnumerable< ModData > data )
|
||||
{
|
||||
Cache = new ModCollectionCache( Name, modDirectory );
|
||||
var changedSettings = false;
|
||||
foreach( var mod in data )
|
||||
{
|
||||
if( Settings.TryGetValue( mod.BasePath.Name, out var settings ) )
|
||||
{
|
||||
return new Mod.Mod( settings, mod );
|
||||
Cache.AddMod( settings, mod, false );
|
||||
}
|
||||
else
|
||||
{
|
||||
changedSettings = true;
|
||||
var newSettings = ModSettings.DefaultSettings( mod.Meta );
|
||||
Settings.Add( mod.BasePath.Name, newSettings );
|
||||
Cache.AddMod( newSettings, mod, false );
|
||||
}
|
||||
}
|
||||
|
||||
var newSettings = ModSettings.DefaultSettings( mod.Meta );
|
||||
Settings.Add( mod.BasePath.Name, newSettings );
|
||||
if( changedSettings )
|
||||
{
|
||||
Save();
|
||||
return new Mod.Mod( newSettings, mod );
|
||||
}
|
||||
|
||||
private bool CleanUnavailableSettings( Dictionary< string, ModData > data )
|
||||
CalculateEffectiveFileList( modDirectory, true, false );
|
||||
}
|
||||
|
||||
public void ClearCache()
|
||||
=> Cache = null;
|
||||
|
||||
public void UpdateSetting( DirectoryInfo modPath, ModMeta meta, bool clear )
|
||||
{
|
||||
if( !Settings.TryGetValue( modPath.Name, out var settings ) )
|
||||
{
|
||||
var removeList = Settings.Where( settingKvp => !data.ContainsKey( settingKvp.Key ) ).ToArray();
|
||||
|
||||
foreach( var s in removeList )
|
||||
{
|
||||
Settings.Remove( s.Key );
|
||||
}
|
||||
|
||||
return removeList.Length > 0;
|
||||
return;
|
||||
}
|
||||
|
||||
public void CreateCache( DirectoryInfo modDirectory, IEnumerable< ModData > data )
|
||||
if( clear )
|
||||
{
|
||||
Cache = new ModCollectionCache( Name, modDirectory );
|
||||
var changedSettings = false;
|
||||
foreach( var mod in data )
|
||||
{
|
||||
if( Settings.TryGetValue( mod.BasePath.Name, out var settings ) )
|
||||
{
|
||||
Cache.AddMod( settings, mod, false );
|
||||
}
|
||||
else
|
||||
{
|
||||
changedSettings = true;
|
||||
var newSettings = ModSettings.DefaultSettings( mod.Meta );
|
||||
Settings.Add( mod.BasePath.Name, newSettings );
|
||||
Cache.AddMod( newSettings, mod, false );
|
||||
}
|
||||
}
|
||||
|
||||
if( changedSettings )
|
||||
{
|
||||
Save();
|
||||
}
|
||||
|
||||
CalculateEffectiveFileList( modDirectory, true, false );
|
||||
settings.Settings.Clear();
|
||||
}
|
||||
|
||||
public void ClearCache()
|
||||
=> Cache = null;
|
||||
|
||||
public void UpdateSetting( DirectoryInfo modPath, ModMeta meta, bool clear )
|
||||
if( settings.FixInvalidSettings( meta ) )
|
||||
{
|
||||
if( !Settings.TryGetValue( modPath.Name, out var settings ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
if (clear)
|
||||
settings.Settings.Clear();
|
||||
if( settings.FixInvalidSettings( meta ) )
|
||||
{
|
||||
Save();
|
||||
}
|
||||
public void UpdateSetting( ModData mod )
|
||||
=> UpdateSetting( mod.BasePath, mod.Meta, false );
|
||||
|
||||
public void UpdateSettings( bool forceSave )
|
||||
{
|
||||
if( Cache == null )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
public void UpdateSetting( ModData mod )
|
||||
=> UpdateSetting( mod.BasePath, mod.Meta, false );
|
||||
|
||||
public void UpdateSettings( bool forceSave )
|
||||
var changes = false;
|
||||
foreach( var mod in Cache.AvailableMods.Values )
|
||||
{
|
||||
if( Cache == null )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var changes = false;
|
||||
foreach( var mod in Cache.AvailableMods.Values )
|
||||
{
|
||||
changes |= mod.FixSettings();
|
||||
}
|
||||
|
||||
if( forceSave || changes )
|
||||
{
|
||||
Save();
|
||||
}
|
||||
changes |= mod.FixSettings();
|
||||
}
|
||||
|
||||
public void CalculateEffectiveFileList( DirectoryInfo modDir, bool withMetaManipulations, bool activeCollection )
|
||||
if( forceSave || changes )
|
||||
{
|
||||
PluginLog.Debug( "Recalculating effective file list for {CollectionName} [{WithMetaManipulations}] [{IsActiveCollection}]", Name,
|
||||
withMetaManipulations, activeCollection );
|
||||
Cache ??= new ModCollectionCache( Name, modDir );
|
||||
UpdateSettings( false );
|
||||
Cache.CalculateEffectiveFileList();
|
||||
if( withMetaManipulations )
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public void CalculateEffectiveFileList( DirectoryInfo modDir, bool withMetaManipulations, bool activeCollection )
|
||||
{
|
||||
PluginLog.Debug( "Recalculating effective file list for {CollectionName} [{WithMetaManipulations}] [{IsActiveCollection}]", Name,
|
||||
withMetaManipulations, activeCollection );
|
||||
Cache ??= new ModCollectionCache( Name, modDir );
|
||||
UpdateSettings( false );
|
||||
Cache.CalculateEffectiveFileList();
|
||||
if( withMetaManipulations )
|
||||
{
|
||||
Cache.UpdateMetaManipulations();
|
||||
if( activeCollection )
|
||||
{
|
||||
Cache.UpdateMetaManipulations();
|
||||
if( activeCollection )
|
||||
{
|
||||
Penumbra.ResidentResources.Reload();
|
||||
}
|
||||
Penumbra.ResidentResources.Reload();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[JsonIgnore]
|
||||
public ModCollectionCache? Cache { get; private set; }
|
||||
[JsonIgnore]
|
||||
public ModCollectionCache? Cache { get; private set; }
|
||||
|
||||
public static ModCollection? LoadFromFile( FileInfo file )
|
||||
public static ModCollection? LoadFromFile( FileInfo file )
|
||||
{
|
||||
if( !file.Exists )
|
||||
{
|
||||
if( !file.Exists )
|
||||
{
|
||||
PluginLog.Error( $"Could not read collection because {file.FullName} does not exist." );
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var collection = JsonConvert.DeserializeObject< ModCollection >( File.ReadAllText( file.FullName ) );
|
||||
return collection;
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
PluginLog.Error( $"Could not read collection information from {file.FullName}:\n{e}" );
|
||||
}
|
||||
|
||||
PluginLog.Error( $"Could not read collection because {file.FullName} does not exist." );
|
||||
return null;
|
||||
}
|
||||
|
||||
private void SaveToFile( FileInfo file )
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
File.WriteAllText( file.FullName, JsonConvert.SerializeObject( this, Formatting.Indented ) );
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
PluginLog.Error( $"Could not write collection {Name} to {file.FullName}:\n{e}" );
|
||||
}
|
||||
var collection = JsonConvert.DeserializeObject< ModCollection >( File.ReadAllText( file.FullName ) );
|
||||
return collection;
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
PluginLog.Error( $"Could not read collection information from {file.FullName}:\n{e}" );
|
||||
}
|
||||
|
||||
public static DirectoryInfo CollectionDir()
|
||||
=> new( Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(), "collections" ) );
|
||||
|
||||
private static FileInfo FileName( DirectoryInfo collectionDir, string name )
|
||||
=> new( Path.Combine( collectionDir.FullName, $"{name.RemoveInvalidPathSymbols()}.json" ) );
|
||||
|
||||
public FileInfo FileName()
|
||||
=> new( Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(),
|
||||
$"{Name.RemoveInvalidPathSymbols()}.json" ) );
|
||||
|
||||
public void Save()
|
||||
{
|
||||
try
|
||||
{
|
||||
var dir = CollectionDir();
|
||||
dir.Create();
|
||||
var file = FileName( dir, Name );
|
||||
SaveToFile( file );
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
PluginLog.Error( $"Could not save collection {Name}:\n{e}" );
|
||||
}
|
||||
}
|
||||
|
||||
public static ModCollection? Load( string name )
|
||||
{
|
||||
var file = FileName( CollectionDir(), name );
|
||||
return file.Exists ? LoadFromFile( file ) : null;
|
||||
}
|
||||
|
||||
public void Delete()
|
||||
{
|
||||
var file = FileName( CollectionDir(), Name );
|
||||
if( file.Exists )
|
||||
{
|
||||
try
|
||||
{
|
||||
file.Delete();
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
PluginLog.Error( $"Could not delete collection file {file} for {Name}:\n{e}" );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void AddMod( ModData data )
|
||||
{
|
||||
if( Cache == null )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Cache.AddMod( Settings.TryGetValue( data.BasePath.Name, out var settings )
|
||||
? settings
|
||||
: ModSettings.DefaultSettings( data.Meta ),
|
||||
data );
|
||||
}
|
||||
|
||||
public string? ResolveSwappedOrReplacementPath( GamePath gameResourcePath )
|
||||
=> Cache?.ResolveSwappedOrReplacementPath( gameResourcePath );
|
||||
|
||||
public static readonly ModCollection Empty = new() { Name = "" };
|
||||
return null;
|
||||
}
|
||||
|
||||
private void SaveToFile( FileInfo file )
|
||||
{
|
||||
try
|
||||
{
|
||||
File.WriteAllText( file.FullName, JsonConvert.SerializeObject( this, Formatting.Indented ) );
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
PluginLog.Error( $"Could not write collection {Name} to {file.FullName}:\n{e}" );
|
||||
}
|
||||
}
|
||||
|
||||
public static DirectoryInfo CollectionDir()
|
||||
=> new(Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(), "collections" ));
|
||||
|
||||
private static FileInfo FileName( DirectoryInfo collectionDir, string name )
|
||||
=> new(Path.Combine( collectionDir.FullName, $"{name.RemoveInvalidPathSymbols()}.json" ));
|
||||
|
||||
public FileInfo FileName()
|
||||
=> new(Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(),
|
||||
$"{Name.RemoveInvalidPathSymbols()}.json" ));
|
||||
|
||||
public void Save()
|
||||
{
|
||||
try
|
||||
{
|
||||
var dir = CollectionDir();
|
||||
dir.Create();
|
||||
var file = FileName( dir, Name );
|
||||
SaveToFile( file );
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
PluginLog.Error( $"Could not save collection {Name}:\n{e}" );
|
||||
}
|
||||
}
|
||||
|
||||
public static ModCollection? Load( string name )
|
||||
{
|
||||
var file = FileName( CollectionDir(), name );
|
||||
return file.Exists ? LoadFromFile( file ) : null;
|
||||
}
|
||||
|
||||
public void Delete()
|
||||
{
|
||||
var file = FileName( CollectionDir(), Name );
|
||||
if( file.Exists )
|
||||
{
|
||||
try
|
||||
{
|
||||
file.Delete();
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
PluginLog.Error( $"Could not delete collection file {file} for {Name}:\n{e}" );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void AddMod( ModData data )
|
||||
{
|
||||
if( Cache == null )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Cache.AddMod( Settings.TryGetValue( data.BasePath.Name, out var settings )
|
||||
? settings
|
||||
: ModSettings.DefaultSettings( data.Meta ),
|
||||
data );
|
||||
}
|
||||
|
||||
public FullPath? ResolveSwappedOrReplacementPath( Utf8GamePath gameResourcePath )
|
||||
=> Cache?.ResolveSwappedOrReplacementPath( gameResourcePath );
|
||||
|
||||
public static readonly ModCollection Empty = new() { Name = "" };
|
||||
}
|
||||
|
|
@ -10,7 +10,6 @@ using Penumbra.GameData.ByteString;
|
|||
using Penumbra.GameData.Util;
|
||||
using Penumbra.Meta;
|
||||
using Penumbra.Mod;
|
||||
using Penumbra.Structs;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.Mods;
|
||||
|
|
@ -20,17 +19,16 @@ namespace Penumbra.Mods;
|
|||
public class ModCollectionCache
|
||||
{
|
||||
// Shared caches to avoid allocations.
|
||||
private static readonly BitArray FileSeen = new(256);
|
||||
private static readonly Dictionary< GamePath, Mod.Mod > RegisteredFiles = new(256);
|
||||
private static readonly BitArray FileSeen = new(256);
|
||||
private static readonly Dictionary< Utf8GamePath, Mod.Mod > RegisteredFiles = new(256);
|
||||
|
||||
public readonly Dictionary< string, Mod.Mod > AvailableMods = new();
|
||||
|
||||
private readonly SortedList< string, object? > _changedItems = new();
|
||||
public readonly Dictionary< GamePath, FullPath > ResolvedFiles = new();
|
||||
public readonly Dictionary< GamePath, GamePath > SwappedFiles = new();
|
||||
public readonly HashSet< FullPath > MissingFiles = new();
|
||||
public readonly HashSet< ulong > Checksums = new();
|
||||
public readonly MetaManager MetaManipulations;
|
||||
private readonly SortedList< string, object? > _changedItems = new();
|
||||
public readonly Dictionary< Utf8GamePath, FullPath > ResolvedFiles = new();
|
||||
public readonly HashSet< FullPath > MissingFiles = new();
|
||||
public readonly HashSet< ulong > Checksums = new();
|
||||
public readonly MetaManager MetaManipulations;
|
||||
|
||||
public IReadOnlyDictionary< string, object? > ChangedItems
|
||||
{
|
||||
|
|
@ -61,7 +59,6 @@ public class ModCollectionCache
|
|||
public void CalculateEffectiveFileList()
|
||||
{
|
||||
ResolvedFiles.Clear();
|
||||
SwappedFiles.Clear();
|
||||
MissingFiles.Clear();
|
||||
RegisteredFiles.Clear();
|
||||
_changedItems.Clear();
|
||||
|
|
@ -85,7 +82,7 @@ public class ModCollectionCache
|
|||
|
||||
private void SetChangedItems()
|
||||
{
|
||||
if( _changedItems.Count > 0 || ResolvedFiles.Count + SwappedFiles.Count + MetaManipulations.Count == 0 )
|
||||
if( _changedItems.Count > 0 || ResolvedFiles.Count + MetaManipulations.Count == 0 )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
@ -98,12 +95,7 @@ public class ModCollectionCache
|
|||
var identifier = GameData.GameData.GetIdentifier();
|
||||
foreach( var resolved in ResolvedFiles.Keys.Where( file => !metaFiles.Contains( file ) ) )
|
||||
{
|
||||
identifier.Identify( _changedItems, resolved );
|
||||
}
|
||||
|
||||
foreach( var swapped in SwappedFiles.Keys )
|
||||
{
|
||||
identifier.Identify( _changedItems, swapped );
|
||||
identifier.Identify( _changedItems, resolved.ToGamePath() );
|
||||
}
|
||||
}
|
||||
catch( Exception e )
|
||||
|
|
@ -134,12 +126,12 @@ public class ModCollectionCache
|
|||
AddRemainingFiles( mod );
|
||||
}
|
||||
|
||||
private bool FilterFile( GamePath gamePath )
|
||||
private static bool FilterFile( Utf8GamePath gamePath )
|
||||
{
|
||||
// If audio streaming is not disabled, replacing .scd files crashes the game,
|
||||
// so only add those files if it is disabled.
|
||||
if( !Penumbra.Config.DisableSoundStreaming
|
||||
&& gamePath.ToString().EndsWith( ".scd", StringComparison.InvariantCultureIgnoreCase ) )
|
||||
&& gamePath.Path.EndsWith( '.', 's', 'c', 'd' ) )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
|
@ -148,7 +140,7 @@ public class ModCollectionCache
|
|||
}
|
||||
|
||||
|
||||
private void AddFile( Mod.Mod mod, GamePath gamePath, FullPath file )
|
||||
private void AddFile( Mod.Mod mod, Utf8GamePath gamePath, FullPath file )
|
||||
{
|
||||
if( FilterFile( gamePath ) )
|
||||
{
|
||||
|
|
@ -187,9 +179,8 @@ public class ModCollectionCache
|
|||
{
|
||||
foreach( var (file, paths) in option.OptionFiles )
|
||||
{
|
||||
var fullPath = new FullPath( mod.Data.BasePath,
|
||||
NewRelPath.FromString( file.ToString(), out var p ) ? p : NewRelPath.Empty ); // TODO
|
||||
var idx = mod.Data.Resources.ModFiles.IndexOf( f => f.Equals( fullPath ) );
|
||||
var fullPath = new FullPath( mod.Data.BasePath, file );
|
||||
var idx = mod.Data.Resources.ModFiles.IndexOf( f => f.Equals( fullPath ) );
|
||||
if( idx < 0 )
|
||||
{
|
||||
AddMissingFile( fullPath );
|
||||
|
|
@ -259,7 +250,7 @@ public class ModCollectionCache
|
|||
{
|
||||
if( file.ToGamePath( mod.Data.BasePath, out var gamePath ) )
|
||||
{
|
||||
AddFile( mod, new GamePath( gamePath.ToString() ), file ); // TODO
|
||||
AddFile( mod, gamePath, file );
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -294,7 +285,7 @@ public class ModCollectionCache
|
|||
if( !RegisteredFiles.TryGetValue( key, out var oldMod ) )
|
||||
{
|
||||
RegisteredFiles.Add( key, mod );
|
||||
SwappedFiles.Add( key, value );
|
||||
ResolvedFiles.Add( key, value );
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -341,54 +332,54 @@ public class ModCollectionCache
|
|||
|
||||
public void RemoveMod( DirectoryInfo basePath )
|
||||
{
|
||||
if( AvailableMods.TryGetValue( basePath.Name, out var mod ) )
|
||||
if( !AvailableMods.TryGetValue( basePath.Name, out var mod ) )
|
||||
{
|
||||
AvailableMods.Remove( basePath.Name );
|
||||
if( mod.Settings.Enabled )
|
||||
{
|
||||
CalculateEffectiveFileList();
|
||||
if( mod.Data.Resources.MetaManipulations.Count > 0 )
|
||||
{
|
||||
UpdateMetaManipulations();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
AvailableMods.Remove( basePath.Name );
|
||||
if( !mod.Settings.Enabled )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CalculateEffectiveFileList();
|
||||
if( mod.Data.Resources.MetaManipulations.Count > 0 )
|
||||
{
|
||||
UpdateMetaManipulations();
|
||||
}
|
||||
}
|
||||
|
||||
private class PriorityComparer : IComparer< Mod.Mod >
|
||||
{
|
||||
public int Compare( Mod.Mod? x, Mod.Mod? y )
|
||||
=> ( x?.Settings.Priority ?? 0 ).CompareTo( y?.Settings.Priority ?? 0 );
|
||||
}
|
||||
|
||||
private static readonly PriorityComparer Comparer = new();
|
||||
|
||||
public void AddMod( ModSettings settings, ModData data, bool updateFileList = true )
|
||||
{
|
||||
if( !AvailableMods.TryGetValue( data.BasePath.Name, out var existingMod ) )
|
||||
if( AvailableMods.ContainsKey( data.BasePath.Name ) )
|
||||
{
|
||||
var newMod = new Mod.Mod( settings, data );
|
||||
AvailableMods[ data.BasePath.Name ] = newMod;
|
||||
return;
|
||||
}
|
||||
|
||||
if( updateFileList && settings.Enabled )
|
||||
{
|
||||
CalculateEffectiveFileList();
|
||||
if( data.Resources.MetaManipulations.Count > 0 )
|
||||
{
|
||||
UpdateMetaManipulations();
|
||||
}
|
||||
}
|
||||
AvailableMods[ data.BasePath.Name ] = new Mod.Mod( settings, data );
|
||||
|
||||
if( !updateFileList || !settings.Enabled )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CalculateEffectiveFileList();
|
||||
if( data.Resources.MetaManipulations.Count > 0 )
|
||||
{
|
||||
UpdateMetaManipulations();
|
||||
}
|
||||
}
|
||||
|
||||
public FullPath? GetCandidateForGameFile( GamePath gameResourcePath )
|
||||
public FullPath? GetCandidateForGameFile( Utf8GamePath gameResourcePath )
|
||||
{
|
||||
if( !ResolvedFiles.TryGetValue( gameResourcePath, out var candidate ) )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if( candidate.FullName.Length >= 260 || !candidate.Exists )
|
||||
if( candidate.InternalName.Length > Utf8GamePath.MaxGamePathLength
|
||||
|| candidate.IsRooted && !candidate.Exists )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
|
@ -396,9 +387,6 @@ public class ModCollectionCache
|
|||
return candidate;
|
||||
}
|
||||
|
||||
public GamePath? GetSwappedFilePath( GamePath gameResourcePath )
|
||||
=> SwappedFiles.TryGetValue( gameResourcePath, out var swappedPath ) ? swappedPath : null;
|
||||
|
||||
public string? ResolveSwappedOrReplacementPath( GamePath gameResourcePath )
|
||||
=> GetCandidateForGameFile( gameResourcePath )?.FullName.Replace( '\\', '/' ) ?? GetSwappedFilePath( gameResourcePath ) ?? null;
|
||||
public FullPath? ResolveSwappedOrReplacementPath( Utf8GamePath gameResourcePath )
|
||||
=> GetCandidateForGameFile( gameResourcePath );
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using Dalamud.Logging;
|
||||
using Penumbra.GameData.ByteString;
|
||||
using Penumbra.GameData.Util;
|
||||
using Penumbra.Meta;
|
||||
using Penumbra.Mod;
|
||||
|
|
@ -347,15 +348,7 @@ namespace Penumbra.Mods
|
|||
return true;
|
||||
}
|
||||
|
||||
public bool CheckCrc64( ulong crc )
|
||||
{
|
||||
if( Collections.ActiveCollection.Cache?.Checksums.Contains( crc ) ?? false )
|
||||
return true;
|
||||
|
||||
return Collections.ForcedCollection.Cache?.Checksums.Contains( crc ) ?? false;
|
||||
}
|
||||
|
||||
public string? ResolveSwappedOrReplacementPath( GamePath gameResourcePath )
|
||||
public FullPath? ResolveSwappedOrReplacementPath( Utf8GamePath gameResourcePath )
|
||||
{
|
||||
var ret = Collections.ActiveCollection.ResolveSwappedOrReplacementPath( gameResourcePath );
|
||||
ret ??= Collections.ForcedCollection.ResolveSwappedOrReplacementPath( gameResourcePath );
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ using System.ComponentModel;
|
|||
using System.IO;
|
||||
using Dalamud.Logging;
|
||||
using Penumbra.Mod;
|
||||
using Penumbra.Structs;
|
||||
|
||||
namespace Penumbra.Mods;
|
||||
|
||||
|
|
|
|||
|
|
@ -18,22 +18,6 @@ using System.Linq;
|
|||
|
||||
namespace Penumbra;
|
||||
|
||||
public class Penumbra2 // : IDalamudPlugin
|
||||
{
|
||||
public string Name
|
||||
=> "Penumbra";
|
||||
|
||||
private const string CommandName = "/penumbra";
|
||||
|
||||
public static Configuration Config { get; private set; } = null!;
|
||||
public static ResourceLoader ResourceLoader { get; private set; } = null!;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
ResourceLoader.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public class Penumbra : IDalamudPlugin
|
||||
{
|
||||
public string Name
|
||||
|
|
@ -54,6 +38,7 @@ public class Penumbra : IDalamudPlugin
|
|||
|
||||
|
||||
public ResourceLoader ResourceLoader { get; }
|
||||
public ResourceLogger ResourceLogger { get; }
|
||||
|
||||
//public PathResolver PathResolver { get; }
|
||||
public SettingsInterface SettingsInterface { get; }
|
||||
|
|
@ -81,19 +66,18 @@ public class Penumbra : IDalamudPlugin
|
|||
CharacterUtility = new CharacterUtility();
|
||||
MetaDefaults = new MetaDefaults();
|
||||
ResourceLoader = new ResourceLoader( this );
|
||||
ResourceLogger = new ResourceLogger( ResourceLoader );
|
||||
PlayerWatcher = PlayerWatchFactory.Create( Dalamud.Framework, Dalamud.ClientState, Dalamud.Objects );
|
||||
ModManager = new ModManager();
|
||||
ModManager.DiscoverMods();
|
||||
//PathResolver = new PathResolver( ResourceLoader, gameUtils );
|
||||
PlayerWatcher = PlayerWatchFactory.Create( Dalamud.Framework, Dalamud.ClientState, Dalamud.Objects );
|
||||
ObjectReloader = new ObjectReloader( ModManager, Config.WaitFrames );
|
||||
//PathResolver = new PathResolver( ResourceLoader, gameUtils );
|
||||
|
||||
Dalamud.Commands.AddHandler( CommandName, new CommandInfo( OnCommand )
|
||||
{
|
||||
HelpMessage = "/penumbra - toggle ui\n/penumbra reload - reload mod file lists & discover any new mods",
|
||||
} );
|
||||
|
||||
ResourceLoader.EnableReplacements();
|
||||
ResourceLoader.EnableLogging();
|
||||
if( Config.DebugMode )
|
||||
{
|
||||
ResourceLoader.EnableDebug();
|
||||
|
|
@ -112,7 +96,7 @@ public class Penumbra : IDalamudPlugin
|
|||
CreateWebServer();
|
||||
}
|
||||
|
||||
if( !Config.EnablePlayerWatch || !Config.IsEnabled )
|
||||
if( !Config.EnablePlayerWatch || !Config.EnableMods )
|
||||
{
|
||||
PlayerWatcher.Disable();
|
||||
}
|
||||
|
|
@ -122,16 +106,25 @@ public class Penumbra : IDalamudPlugin
|
|||
PluginLog.Debug( "Triggered Redraw of {Player}.", p.Name );
|
||||
ObjectReloader.RedrawObject( p, RedrawType.OnlyWithSettings );
|
||||
};
|
||||
|
||||
ResourceLoader.EnableHooks();
|
||||
if (Config.EnableMods)
|
||||
ResourceLoader.EnableReplacements();
|
||||
if (Config.DebugMode)
|
||||
ResourceLoader.EnableDebug();
|
||||
if (Config.EnableFullResourceLogging)
|
||||
ResourceLoader.EnableFullLogging();
|
||||
}
|
||||
|
||||
public bool Enable()
|
||||
{
|
||||
if( Config.IsEnabled )
|
||||
if( Config.EnableMods )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Config.IsEnabled = true;
|
||||
Config.EnableMods = true;
|
||||
ResourceLoader.EnableReplacements();
|
||||
ResidentResources.Reload();
|
||||
if( Config.EnablePlayerWatch )
|
||||
{
|
||||
|
|
@ -145,12 +138,13 @@ public class Penumbra : IDalamudPlugin
|
|||
|
||||
public bool Disable()
|
||||
{
|
||||
if( !Config.IsEnabled )
|
||||
if( !Config.EnableMods )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Config.IsEnabled = false;
|
||||
Config.EnableMods = false;
|
||||
ResourceLoader.DisableReplacements();
|
||||
ResidentResources.Reload();
|
||||
if( Config.EnablePlayerWatch )
|
||||
{
|
||||
|
|
@ -219,8 +213,10 @@ public class Penumbra : IDalamudPlugin
|
|||
Dalamud.Commands.RemoveHandler( CommandName );
|
||||
|
||||
//PathResolver.Dispose();
|
||||
ResourceLogger.Dispose();
|
||||
ResourceLoader.Dispose();
|
||||
|
||||
|
||||
ShutdownWebServer();
|
||||
}
|
||||
|
||||
|
|
@ -322,8 +318,8 @@ public class Penumbra : IDalamudPlugin
|
|||
}
|
||||
case "toggle":
|
||||
{
|
||||
SetEnabled( !Config.IsEnabled );
|
||||
Dalamud.Chat.Print( Config.IsEnabled
|
||||
SetEnabled( !Config.EnableMods );
|
||||
Dalamud.Chat.Print( Config.EnableMods
|
||||
? modsEnabled
|
||||
: modsDisabled );
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -1,103 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using Newtonsoft.Json;
|
||||
using Penumbra.GameData.Util;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.Structs
|
||||
{
|
||||
public enum SelectType
|
||||
{
|
||||
Single,
|
||||
Multi,
|
||||
}
|
||||
|
||||
public struct Option
|
||||
{
|
||||
public string OptionName;
|
||||
public string OptionDesc;
|
||||
|
||||
[JsonProperty( ItemConverterType = typeof( SingleOrArrayConverter< GamePath > ) )]
|
||||
public Dictionary< RelPath, HashSet< GamePath > > OptionFiles;
|
||||
|
||||
public bool AddFile( RelPath filePath, GamePath gamePath )
|
||||
{
|
||||
if( OptionFiles.TryGetValue( filePath, out var set ) )
|
||||
{
|
||||
return set.Add( gamePath );
|
||||
}
|
||||
|
||||
OptionFiles[ filePath ] = new HashSet< GamePath >() { gamePath };
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public struct OptionGroup
|
||||
{
|
||||
public string GroupName;
|
||||
|
||||
[JsonConverter( typeof( Newtonsoft.Json.Converters.StringEnumConverter ) )]
|
||||
public SelectType SelectionType;
|
||||
|
||||
public List< Option > Options;
|
||||
|
||||
private bool ApplySingleGroupFiles( RelPath relPath, int selection, HashSet< GamePath > paths )
|
||||
{
|
||||
// Selection contains the path, merge all GamePaths for this config.
|
||||
if( Options[ selection ].OptionFiles.TryGetValue( relPath, out var groupPaths ) )
|
||||
{
|
||||
paths.UnionWith( groupPaths );
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the group contains the file in another selection, return true to skip it for default files.
|
||||
for( var i = 0; i < Options.Count; ++i )
|
||||
{
|
||||
if( i == selection )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if( Options[ i ].OptionFiles.ContainsKey( relPath ) )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool ApplyMultiGroupFiles( RelPath relPath, int selection, HashSet< GamePath > paths )
|
||||
{
|
||||
var doNotAdd = false;
|
||||
for( var i = 0; i < Options.Count; ++i )
|
||||
{
|
||||
if( ( selection & ( 1 << i ) ) != 0 )
|
||||
{
|
||||
if( Options[ i ].OptionFiles.TryGetValue( relPath, out var groupPaths ) )
|
||||
{
|
||||
paths.UnionWith( groupPaths );
|
||||
doNotAdd = true;
|
||||
}
|
||||
}
|
||||
else if( Options[ i ].OptionFiles.ContainsKey( relPath ) )
|
||||
{
|
||||
doNotAdd = true;
|
||||
}
|
||||
}
|
||||
|
||||
return doNotAdd;
|
||||
}
|
||||
|
||||
// Adds all game paths from the given option that correspond to the given RelPath to paths, if any exist.
|
||||
internal bool ApplyGroupFiles( RelPath relPath, int selection, HashSet< GamePath > paths )
|
||||
{
|
||||
return SelectionType switch
|
||||
{
|
||||
SelectType.Single => ApplySingleGroupFiles( relPath, selection, paths ),
|
||||
SelectType.Multi => ApplyMultiGroupFiles( relPath, selection, paths ),
|
||||
_ => throw new InvalidEnumArgumentException( "Invalid option group type." ),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
using System.Numerics;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Windows.Forms;
|
||||
using Dalamud.Interface;
|
||||
using ImGuiNET;
|
||||
using Penumbra.GameData.ByteString;
|
||||
|
||||
namespace Penumbra.UI.Custom
|
||||
{
|
||||
|
|
@ -20,6 +20,19 @@ namespace Penumbra.UI.Custom
|
|||
ImGui.SetTooltip( "Click to copy to clipboard." );
|
||||
}
|
||||
}
|
||||
|
||||
public static unsafe void CopyOnClickSelectable( Utf8String text )
|
||||
{
|
||||
if( ImGuiNative.igSelectable_Bool( text.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero ) != 0 )
|
||||
{
|
||||
ImGuiNative.igSetClipboardText( text.Path );
|
||||
}
|
||||
|
||||
if( ImGui.IsItemHovered() )
|
||||
{
|
||||
ImGui.SetTooltip( "Click to copy to clipboard." );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static partial class ImGuiCustom
|
||||
|
|
|
|||
|
|
@ -21,11 +21,21 @@ public partial class SettingsInterface
|
|||
private string _filePathFilter = string.Empty;
|
||||
private string _filePathFilterLower = string.Empty;
|
||||
|
||||
private readonly float _leftTextLength =
|
||||
ImGui.CalcTextSize( "chara/human/c0000/obj/body/b0000/material/v0000/mt_c0000b0000_b.mtrl" ).X / ImGuiHelpers.GlobalScale + 40;
|
||||
private const float LeftTextLength = 600;
|
||||
|
||||
private float _arrowLength = 0;
|
||||
|
||||
private static void DrawLine( Utf8GamePath path, FullPath name )
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ImGuiCustom.CopyOnClickSelectable( path.Path );
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGuiCustom.PrintIcon( FontAwesomeIcon.LongArrowAltLeft );
|
||||
ImGui.SameLine();
|
||||
ImGuiCustom.CopyOnClickSelectable( name.InternalName );
|
||||
}
|
||||
|
||||
private static void DrawLine( string path, string name )
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
|
|
@ -45,13 +55,13 @@ public partial class SettingsInterface
|
|||
_arrowLength = ImGui.CalcTextSize( FontAwesomeIcon.LongArrowAltLeft.ToIconString() ).X / ImGuiHelpers.GlobalScale;
|
||||
}
|
||||
|
||||
ImGui.SetNextItemWidth( _leftTextLength * ImGuiHelpers.GlobalScale );
|
||||
ImGui.SetNextItemWidth( LeftTextLength * ImGuiHelpers.GlobalScale );
|
||||
if( ImGui.InputTextWithHint( "##effective_changes_gfilter", "Filter game path...", ref _gamePathFilter, 256 ) )
|
||||
{
|
||||
_gamePathFilterLower = _gamePathFilter.ToLowerInvariant();
|
||||
}
|
||||
|
||||
ImGui.SameLine( ( _leftTextLength + _arrowLength ) * ImGuiHelpers.GlobalScale + 3 * ImGui.GetStyle().ItemSpacing.X );
|
||||
ImGui.SameLine( ( LeftTextLength + _arrowLength ) * ImGuiHelpers.GlobalScale + 3 * ImGui.GetStyle().ItemSpacing.X );
|
||||
ImGui.SetNextItemWidth( -1 );
|
||||
if( ImGui.InputTextWithHint( "##effective_changes_ffilter", "Filter file path...", ref _filePathFilter, 256 ) )
|
||||
{
|
||||
|
|
@ -59,7 +69,7 @@ public partial class SettingsInterface
|
|||
}
|
||||
}
|
||||
|
||||
private bool CheckFilters( KeyValuePair< GamePath, FullPath > kvp )
|
||||
private bool CheckFilters( KeyValuePair< Utf8GamePath, FullPath > kvp )
|
||||
{
|
||||
if( _gamePathFilter.Any() && !kvp.Key.ToString().Contains( _gamePathFilterLower ) )
|
||||
{
|
||||
|
|
@ -69,7 +79,7 @@ public partial class SettingsInterface
|
|||
return !_filePathFilter.Any() || kvp.Value.FullName.ToLowerInvariant().Contains( _filePathFilterLower );
|
||||
}
|
||||
|
||||
private bool CheckFilters( KeyValuePair< GamePath, GamePath > kvp )
|
||||
private bool CheckFilters( KeyValuePair< Utf8GamePath, Utf8GamePath > kvp )
|
||||
{
|
||||
if( _gamePathFilter.Any() && !kvp.Key.ToString().Contains( _gamePathFilterLower ) )
|
||||
{
|
||||
|
|
@ -94,11 +104,6 @@ public partial class SettingsInterface
|
|||
void DrawFileLines( ModCollectionCache cache )
|
||||
{
|
||||
foreach( var (gp, fp) in cache.ResolvedFiles.Where( CheckFilters ) )
|
||||
{
|
||||
DrawLine( gp, fp.FullName );
|
||||
}
|
||||
|
||||
foreach( var (gp, fp) in cache.SwappedFiles.Where( CheckFilters ) )
|
||||
{
|
||||
DrawLine( gp, fp );
|
||||
}
|
||||
|
|
@ -139,75 +144,67 @@ public partial class SettingsInterface
|
|||
var activeCollection = modManager.Collections.ActiveCollection.Cache;
|
||||
var forcedCollection = modManager.Collections.ForcedCollection.Cache;
|
||||
|
||||
var (activeResolved, activeSwap, activeMeta) = activeCollection != null
|
||||
? ( activeCollection.ResolvedFiles.Count, activeCollection.SwappedFiles.Count, activeCollection.MetaManipulations.Count )
|
||||
: ( 0, 0, 0 );
|
||||
var (forcedResolved, forcedSwap, forcedMeta) = forcedCollection != null
|
||||
? ( forcedCollection.ResolvedFiles.Count, forcedCollection.SwappedFiles.Count, forcedCollection.MetaManipulations.Count )
|
||||
: ( 0, 0, 0 );
|
||||
var totalLines = activeResolved + forcedResolved + activeSwap + forcedSwap + activeMeta + forcedMeta;
|
||||
var (activeResolved, activeMeta) = activeCollection != null
|
||||
? ( activeCollection.ResolvedFiles.Count, activeCollection.MetaManipulations.Count )
|
||||
: ( 0, 0 );
|
||||
var (forcedResolved, forcedMeta) = forcedCollection != null
|
||||
? ( forcedCollection.ResolvedFiles.Count, forcedCollection.MetaManipulations.Count )
|
||||
: ( 0, 0 );
|
||||
var totalLines = activeResolved + forcedResolved + activeMeta + forcedMeta;
|
||||
if( totalLines == 0 )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if( ImGui.BeginTable( "##effective_changes", 2, flags, AutoFillSize ) )
|
||||
if( !ImGui.BeginTable( "##effective_changes", 2, flags, AutoFillSize ) )
|
||||
{
|
||||
raii.Push( ImGui.EndTable );
|
||||
ImGui.TableSetupColumn( "##tableGamePathCol", ImGuiTableColumnFlags.None, _leftTextLength * ImGuiHelpers.GlobalScale );
|
||||
return;
|
||||
}
|
||||
|
||||
if( _filePathFilter.Any() || _gamePathFilter.Any() )
|
||||
raii.Push( ImGui.EndTable );
|
||||
ImGui.TableSetupColumn( "##tableGamePathCol", ImGuiTableColumnFlags.None, LeftTextLength * ImGuiHelpers.GlobalScale );
|
||||
|
||||
if( _filePathFilter.Length > 0 || _gamePathFilter.Length > 0 )
|
||||
{
|
||||
DrawFilteredRows( activeCollection, forcedCollection );
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGuiListClipperPtr clipper;
|
||||
unsafe
|
||||
{
|
||||
DrawFilteredRows( activeCollection, forcedCollection );
|
||||
clipper = new ImGuiListClipperPtr( ImGuiNative.ImGuiListClipper_ImGuiListClipper() );
|
||||
}
|
||||
else
|
||||
|
||||
clipper.Begin( totalLines );
|
||||
|
||||
|
||||
while( clipper.Step() )
|
||||
{
|
||||
ImGuiListClipperPtr clipper;
|
||||
unsafe
|
||||
for( var actualRow = clipper.DisplayStart; actualRow < clipper.DisplayEnd; actualRow++ )
|
||||
{
|
||||
clipper = new ImGuiListClipperPtr( ImGuiNative.ImGuiListClipper_ImGuiListClipper() );
|
||||
}
|
||||
|
||||
clipper.Begin( totalLines );
|
||||
|
||||
|
||||
while( clipper.Step() )
|
||||
{
|
||||
for( var actualRow = clipper.DisplayStart; actualRow < clipper.DisplayEnd; actualRow++ )
|
||||
var row = actualRow;
|
||||
ImGui.TableNextRow();
|
||||
if( row < activeResolved )
|
||||
{
|
||||
var row = actualRow;
|
||||
ImGui.TableNextRow();
|
||||
if( row < activeResolved )
|
||||
{
|
||||
var (gamePath, file) = activeCollection!.ResolvedFiles.ElementAt( row );
|
||||
DrawLine( gamePath, file.FullName );
|
||||
}
|
||||
else if( ( row -= activeResolved ) < activeSwap )
|
||||
{
|
||||
var (gamePath, swap) = activeCollection!.SwappedFiles.ElementAt( row );
|
||||
DrawLine( gamePath, swap );
|
||||
}
|
||||
else if( ( row -= activeSwap ) < activeMeta )
|
||||
{
|
||||
var (manip, mod) = activeCollection!.MetaManipulations.Manipulations.ElementAt( row );
|
||||
DrawLine( manip.IdentifierString(), mod.Data.Meta.Name );
|
||||
}
|
||||
else if( ( row -= activeMeta ) < forcedResolved )
|
||||
{
|
||||
var (gamePath, file) = forcedCollection!.ResolvedFiles.ElementAt( row );
|
||||
DrawLine( gamePath, file.FullName );
|
||||
}
|
||||
else if( ( row -= forcedResolved ) < forcedSwap )
|
||||
{
|
||||
var (gamePath, swap) = forcedCollection!.SwappedFiles.ElementAt( row );
|
||||
DrawLine( gamePath, swap );
|
||||
}
|
||||
else
|
||||
{
|
||||
row -= forcedSwap;
|
||||
var (manip, mod) = forcedCollection!.MetaManipulations.Manipulations.ElementAt( row );
|
||||
DrawLine( manip.IdentifierString(), mod.Data.Meta.Name );
|
||||
}
|
||||
var (gamePath, file) = activeCollection!.ResolvedFiles.ElementAt( row );
|
||||
DrawLine( gamePath, file );
|
||||
}
|
||||
else if( ( row -= activeResolved ) < activeMeta )
|
||||
{
|
||||
var (manip, mod) = activeCollection!.MetaManipulations.Manipulations.ElementAt( row );
|
||||
DrawLine( manip.IdentifierString(), mod.Data.Meta.Name );
|
||||
}
|
||||
else if( ( row -= activeMeta ) < forcedResolved )
|
||||
{
|
||||
var (gamePath, file) = forcedCollection!.ResolvedFiles.ElementAt( row );
|
||||
DrawLine( gamePath, file );
|
||||
}
|
||||
else
|
||||
{
|
||||
row -= forcedResolved;
|
||||
var (manip, mod) = forcedCollection!.MetaManipulations.Manipulations.ElementAt( row );
|
||||
DrawLine( manip.IdentifierString(), mod.Data.Meta.Name );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ using Penumbra.GameData.Util;
|
|||
using Penumbra.Meta;
|
||||
using Penumbra.Mod;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Structs;
|
||||
using Penumbra.UI.Custom;
|
||||
using Penumbra.Util;
|
||||
using ImGui = ImGuiNET.ImGui;
|
||||
|
|
@ -56,7 +55,7 @@ public partial class SettingsInterface
|
|||
private Option? _selectedOption;
|
||||
private string _currentGamePaths = "";
|
||||
|
||||
private (FullPath name, bool selected, uint color, RelPath relName)[]? _fullFilenameList;
|
||||
private (FullPath name, bool selected, uint color, Utf8RelPath relName)[]? _fullFilenameList;
|
||||
|
||||
private readonly Selector _selector;
|
||||
private readonly SettingsInterface _base;
|
||||
|
|
@ -218,7 +217,10 @@ public partial class SettingsInterface
|
|||
indent.Push( 15f );
|
||||
foreach( var file in files )
|
||||
{
|
||||
ImGui.Selectable( file );
|
||||
unsafe
|
||||
{
|
||||
ImGuiNative.igSelectable_Bool( file.Path.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero );
|
||||
}
|
||||
}
|
||||
|
||||
foreach( var manip in manipulations )
|
||||
|
|
@ -258,13 +260,13 @@ public partial class SettingsInterface
|
|||
foreach( var (source, target) in Meta.FileSwaps )
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ImGuiCustom.CopyOnClickSelectable( source );
|
||||
ImGuiCustom.CopyOnClickSelectable( source.Path );
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGuiCustom.PrintIcon( FontAwesomeIcon.LongArrowAltRight );
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGuiCustom.CopyOnClickSelectable( target );
|
||||
ImGuiCustom.CopyOnClickSelectable( target.InternalName );
|
||||
|
||||
ImGui.TableNextRow();
|
||||
}
|
||||
|
|
@ -278,7 +280,8 @@ public partial class SettingsInterface
|
|||
}
|
||||
|
||||
_fullFilenameList = Mod.Data.Resources.ModFiles
|
||||
.Select( f => ( f, false, ColorGreen, new RelPath( f, Mod.Data.BasePath ) ) ).ToArray();
|
||||
.Select( f => ( f, false, ColorGreen, Utf8RelPath.FromFile( f, Mod.Data.BasePath, out var p ) ? p : Utf8RelPath.Empty ) )
|
||||
.ToArray();
|
||||
|
||||
if( Meta.Groups.Count == 0 )
|
||||
{
|
||||
|
|
@ -339,24 +342,23 @@ public partial class SettingsInterface
|
|||
}
|
||||
}
|
||||
|
||||
private static int HandleDefaultString( GamePath[] gamePaths, out int removeFolders )
|
||||
private static int HandleDefaultString( Utf8GamePath[] gamePaths, out int removeFolders )
|
||||
{
|
||||
removeFolders = 0;
|
||||
var defaultIndex =
|
||||
gamePaths.IndexOf( p => ( ( string )p ).StartsWith( TextDefaultGamePath ) );
|
||||
var defaultIndex = gamePaths.IndexOf( p => p.Path.StartsWith( DefaultUtf8GamePath ) );
|
||||
if( defaultIndex < 0 )
|
||||
{
|
||||
return defaultIndex;
|
||||
}
|
||||
|
||||
string path = gamePaths[ defaultIndex ];
|
||||
var path = gamePaths[ defaultIndex ].Path;
|
||||
if( path.Length == TextDefaultGamePath.Length )
|
||||
{
|
||||
return defaultIndex;
|
||||
}
|
||||
|
||||
if( path[ TextDefaultGamePath.Length ] != '-'
|
||||
|| !int.TryParse( path.Substring( TextDefaultGamePath.Length + 1 ), out removeFolders ) )
|
||||
if( path[ TextDefaultGamePath.Length ] != ( byte )'-'
|
||||
|| !int.TryParse( path.Substring( TextDefaultGamePath.Length + 1 ).ToString(), out removeFolders ) )
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
|
@ -373,8 +375,9 @@ public partial class SettingsInterface
|
|||
|
||||
var option = ( Option )_selectedOption;
|
||||
|
||||
var gamePaths = _currentGamePaths.Split( ';' ).Select( p => new GamePath( p ) ).ToArray();
|
||||
if( gamePaths.Length == 0 || ( ( string )gamePaths[ 0 ] ).Length == 0 )
|
||||
var gamePaths = _currentGamePaths.Split( ';' )
|
||||
.Select( p => Utf8GamePath.FromString( p, out var path, false ) ? path : Utf8GamePath.Empty ).Where( p => !p.IsEmpty ).ToArray();
|
||||
if( gamePaths.Length == 0 )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
@ -517,18 +520,18 @@ public partial class SettingsInterface
|
|||
{
|
||||
Selectable( 0, ColorGreen );
|
||||
|
||||
using var indent = ImGuiRaii.PushIndent( indentWidth );
|
||||
var tmpPaths = gamePaths.ToArray();
|
||||
foreach( var gamePath in tmpPaths )
|
||||
using var indent = ImGuiRaii.PushIndent( indentWidth );
|
||||
foreach( var gamePath in gamePaths.ToArray() )
|
||||
{
|
||||
string tmp = gamePath;
|
||||
var tmp = gamePath.ToString();
|
||||
var old = tmp;
|
||||
if( ImGui.InputText( $"##{fileName}_{gamePath}", ref tmp, 128, ImGuiInputTextFlags.EnterReturnsTrue )
|
||||
&& tmp != gamePath )
|
||||
&& tmp != old )
|
||||
{
|
||||
gamePaths.Remove( gamePath );
|
||||
if( tmp.Length > 0 )
|
||||
if( tmp.Length > 0 && Utf8GamePath.FromString( tmp, out var p, true ) )
|
||||
{
|
||||
gamePaths.Add( new GamePath( tmp ) );
|
||||
gamePaths.Add( p );
|
||||
}
|
||||
else if( gamePaths.Count == 0 )
|
||||
{
|
||||
|
|
|
|||
|
|
@ -3,136 +3,179 @@ using System.Linq;
|
|||
using System.Numerics;
|
||||
using Dalamud.Interface;
|
||||
using ImGuiNET;
|
||||
using Penumbra.GameData.ByteString;
|
||||
using Penumbra.GameData.Util;
|
||||
using Penumbra.Mod;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Structs;
|
||||
using Penumbra.UI.Custom;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.UI
|
||||
namespace Penumbra.UI;
|
||||
|
||||
public partial class SettingsInterface
|
||||
{
|
||||
public partial class SettingsInterface
|
||||
private partial class PluginDetails
|
||||
{
|
||||
private partial class PluginDetails
|
||||
private const string LabelDescEdit = "##descedit";
|
||||
private const string LabelNewSingleGroupEdit = "##newSingleGroup";
|
||||
private const string LabelNewMultiGroup = "##newMultiGroup";
|
||||
private const string LabelGamePathsEditBox = "##gamePathsEdit";
|
||||
private const string ButtonAddToGroup = "Add to Group";
|
||||
private const string ButtonRemoveFromGroup = "Remove from Group";
|
||||
private const string TooltipAboutEdit = "Use Ctrl+Enter for newlines.";
|
||||
private const string TextNoOptionAvailable = "[Not Available]";
|
||||
private const string TextDefaultGamePath = "default";
|
||||
private static readonly Utf8String DefaultUtf8GamePath = Utf8String.FromStringUnsafe( TextDefaultGamePath, true );
|
||||
private const char GamePathsSeparator = ';';
|
||||
|
||||
private static readonly string TooltipFilesTabEdit =
|
||||
$"{TooltipFilesTab}\n"
|
||||
+ $"Red Files are replaced in another group or a different option in this group, but not contained in the current option.";
|
||||
|
||||
private static readonly string TooltipGamePathsEdit =
|
||||
$"Enter all game paths to add or remove, separated by '{GamePathsSeparator}'.\n"
|
||||
+ $"Use '{TextDefaultGamePath}' to add the original file path."
|
||||
+ $"Use '{TextDefaultGamePath}-#' to skip the first # relative directories.";
|
||||
|
||||
private const float MultiEditBoxWidth = 300f;
|
||||
|
||||
private bool DrawEditGroupSelector()
|
||||
{
|
||||
private const string LabelDescEdit = "##descedit";
|
||||
private const string LabelNewSingleGroupEdit = "##newSingleGroup";
|
||||
private const string LabelNewMultiGroup = "##newMultiGroup";
|
||||
private const string LabelGamePathsEditBox = "##gamePathsEdit";
|
||||
private const string ButtonAddToGroup = "Add to Group";
|
||||
private const string ButtonRemoveFromGroup = "Remove from Group";
|
||||
private const string TooltipAboutEdit = "Use Ctrl+Enter for newlines.";
|
||||
private const string TextNoOptionAvailable = "[Not Available]";
|
||||
private const string TextDefaultGamePath = "default";
|
||||
private const char GamePathsSeparator = ';';
|
||||
|
||||
private static readonly string TooltipFilesTabEdit =
|
||||
$"{TooltipFilesTab}\n"
|
||||
+ $"Red Files are replaced in another group or a different option in this group, but not contained in the current option.";
|
||||
|
||||
private static readonly string TooltipGamePathsEdit =
|
||||
$"Enter all game paths to add or remove, separated by '{GamePathsSeparator}'.\n"
|
||||
+ $"Use '{TextDefaultGamePath}' to add the original file path."
|
||||
+ $"Use '{TextDefaultGamePath}-#' to skip the first # relative directories.";
|
||||
|
||||
private const float MultiEditBoxWidth = 300f;
|
||||
|
||||
private bool DrawEditGroupSelector()
|
||||
ImGui.SetNextItemWidth( OptionSelectionWidth * ImGuiHelpers.GlobalScale );
|
||||
if( Meta!.Groups.Count == 0 )
|
||||
{
|
||||
ImGui.SetNextItemWidth( OptionSelectionWidth * ImGuiHelpers.GlobalScale );
|
||||
if( Meta!.Groups.Count == 0 )
|
||||
{
|
||||
ImGui.Combo( LabelGroupSelect, ref _selectedGroupIndex, TextNoOptionAvailable, 1 );
|
||||
return false;
|
||||
}
|
||||
|
||||
if( ImGui.Combo( LabelGroupSelect, ref _selectedGroupIndex
|
||||
, Meta.Groups.Values.Select( g => g.GroupName ).ToArray()
|
||||
, Meta.Groups.Count ) )
|
||||
{
|
||||
SelectGroup();
|
||||
SelectOption( 0 );
|
||||
}
|
||||
|
||||
return true;
|
||||
ImGui.Combo( LabelGroupSelect, ref _selectedGroupIndex, TextNoOptionAvailable, 1 );
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool DrawEditOptionSelector()
|
||||
if( ImGui.Combo( LabelGroupSelect, ref _selectedGroupIndex
|
||||
, Meta.Groups.Values.Select( g => g.GroupName ).ToArray()
|
||||
, Meta.Groups.Count ) )
|
||||
{
|
||||
SelectGroup();
|
||||
SelectOption( 0 );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool DrawEditOptionSelector()
|
||||
{
|
||||
ImGui.SameLine();
|
||||
ImGui.SetNextItemWidth( OptionSelectionWidth );
|
||||
if( ( _selectedGroup?.Options.Count ?? 0 ) == 0 )
|
||||
{
|
||||
ImGui.Combo( LabelOptionSelect, ref _selectedOptionIndex, TextNoOptionAvailable, 1 );
|
||||
return false;
|
||||
}
|
||||
|
||||
var group = ( OptionGroup )_selectedGroup!;
|
||||
if( ImGui.Combo( LabelOptionSelect, ref _selectedOptionIndex, group.Options.Select( o => o.OptionName ).ToArray(),
|
||||
group.Options.Count ) )
|
||||
{
|
||||
SelectOption();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void DrawFileListTabEdit()
|
||||
{
|
||||
if( ImGui.BeginTabItem( LabelFileListTab ) )
|
||||
{
|
||||
UpdateFilenameList();
|
||||
if( ImGui.IsItemHovered() )
|
||||
{
|
||||
ImGui.SetTooltip( _editMode ? TooltipFilesTabEdit : TooltipFilesTab );
|
||||
}
|
||||
|
||||
ImGui.SetNextItemWidth( -1 );
|
||||
if( ImGui.BeginListBox( LabelFileListHeader, AutoFillSize - Vector2.UnitY * 1.5f * ImGui.GetTextLineHeight() ) )
|
||||
{
|
||||
for( var i = 0; i < Mod!.Data.Resources.ModFiles.Count; ++i )
|
||||
{
|
||||
DrawFileAndGamePaths( i );
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.EndListBox();
|
||||
|
||||
DrawGroupRow();
|
||||
ImGui.EndTabItem();
|
||||
}
|
||||
else
|
||||
{
|
||||
_fullFilenameList = null;
|
||||
}
|
||||
}
|
||||
|
||||
private ImGuiRaii.EndStack DrawMultiSelectorEditBegin( OptionGroup group )
|
||||
{
|
||||
var groupName = group.GroupName;
|
||||
if( ImGuiCustom.BeginFramedGroupEdit( ref groupName ) )
|
||||
{
|
||||
if( Penumbra.ModManager.ChangeModGroup( group.GroupName, groupName, Mod.Data ) && Mod.Data.Meta.RefreshHasGroupsWithConfig() )
|
||||
{
|
||||
_selector.Cache.TriggerFilterReset();
|
||||
}
|
||||
}
|
||||
|
||||
return ImGuiRaii.DeferredEnd( ImGuiCustom.EndFramedGroup );
|
||||
}
|
||||
|
||||
private void DrawMultiSelectorEditAdd( OptionGroup group, float nameBoxStart )
|
||||
{
|
||||
var newOption = "";
|
||||
ImGui.SetCursorPosX( nameBoxStart );
|
||||
ImGui.SetNextItemWidth( MultiEditBoxWidth * ImGuiHelpers.GlobalScale );
|
||||
if( ImGui.InputTextWithHint( $"##new_{group.GroupName}_l", "Add new option...", ref newOption, 64,
|
||||
ImGuiInputTextFlags.EnterReturnsTrue )
|
||||
&& newOption.Length != 0 )
|
||||
{
|
||||
group.Options.Add( new Option()
|
||||
{ OptionName = newOption, OptionDesc = "", OptionFiles = new Dictionary< Utf8RelPath, HashSet< Utf8GamePath > >() } );
|
||||
_selector.SaveCurrentMod();
|
||||
if( Mod!.Data.Meta.RefreshHasGroupsWithConfig() )
|
||||
{
|
||||
_selector.Cache.TriggerFilterReset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawMultiSelectorEdit( OptionGroup group )
|
||||
{
|
||||
var nameBoxStart = CheckMarkSize;
|
||||
var flag = Mod!.Settings.Settings[ group.GroupName ];
|
||||
|
||||
using var raii = DrawMultiSelectorEditBegin( group );
|
||||
for( var i = 0; i < group.Options.Count; ++i )
|
||||
{
|
||||
var opt = group.Options[ i ];
|
||||
var label = $"##{group.GroupName}_{i}";
|
||||
DrawMultiSelectorCheckBox( group, i, flag, label );
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.SetNextItemWidth( OptionSelectionWidth );
|
||||
if( ( _selectedGroup?.Options.Count ?? 0 ) == 0 )
|
||||
var newName = opt.OptionName;
|
||||
|
||||
if( nameBoxStart == CheckMarkSize )
|
||||
{
|
||||
ImGui.Combo( LabelOptionSelect, ref _selectedOptionIndex, TextNoOptionAvailable, 1 );
|
||||
return false;
|
||||
nameBoxStart = ImGui.GetCursorPosX();
|
||||
}
|
||||
|
||||
var group = ( OptionGroup )_selectedGroup!;
|
||||
if( ImGui.Combo( LabelOptionSelect, ref _selectedOptionIndex, group.Options.Select( o => o.OptionName ).ToArray(),
|
||||
group.Options.Count ) )
|
||||
{
|
||||
SelectOption();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void DrawFileListTabEdit()
|
||||
{
|
||||
if( ImGui.BeginTabItem( LabelFileListTab ) )
|
||||
{
|
||||
UpdateFilenameList();
|
||||
if( ImGui.IsItemHovered() )
|
||||
{
|
||||
ImGui.SetTooltip( _editMode ? TooltipFilesTabEdit : TooltipFilesTab );
|
||||
}
|
||||
|
||||
ImGui.SetNextItemWidth( -1 );
|
||||
if( ImGui.BeginListBox( LabelFileListHeader, AutoFillSize - Vector2.UnitY * 1.5f * ImGui.GetTextLineHeight() ) )
|
||||
{
|
||||
for( var i = 0; i < Mod!.Data.Resources.ModFiles.Count; ++i )
|
||||
{
|
||||
DrawFileAndGamePaths( i );
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.EndListBox();
|
||||
|
||||
DrawGroupRow();
|
||||
ImGui.EndTabItem();
|
||||
}
|
||||
else
|
||||
{
|
||||
_fullFilenameList = null;
|
||||
}
|
||||
}
|
||||
|
||||
private ImGuiRaii.EndStack DrawMultiSelectorEditBegin( OptionGroup group )
|
||||
{
|
||||
var groupName = group.GroupName;
|
||||
if( ImGuiCustom.BeginFramedGroupEdit( ref groupName ) )
|
||||
{
|
||||
if( Penumbra.ModManager.ChangeModGroup( group.GroupName, groupName, Mod.Data ) && Mod.Data.Meta.RefreshHasGroupsWithConfig() )
|
||||
{
|
||||
_selector.Cache.TriggerFilterReset();
|
||||
}
|
||||
}
|
||||
|
||||
return ImGuiRaii.DeferredEnd( ImGuiCustom.EndFramedGroup );
|
||||
}
|
||||
|
||||
private void DrawMultiSelectorEditAdd( OptionGroup group, float nameBoxStart )
|
||||
{
|
||||
var newOption = "";
|
||||
ImGui.SetCursorPosX( nameBoxStart );
|
||||
ImGui.SetNextItemWidth( MultiEditBoxWidth * ImGuiHelpers.GlobalScale );
|
||||
if( ImGui.InputTextWithHint( $"##new_{group.GroupName}_l", "Add new option...", ref newOption, 64,
|
||||
ImGuiInputTextFlags.EnterReturnsTrue )
|
||||
&& newOption.Length != 0 )
|
||||
if( ImGui.InputText( $"{label}_l", ref newName, 64, ImGuiInputTextFlags.EnterReturnsTrue ) )
|
||||
{
|
||||
group.Options.Add( new Option()
|
||||
{ OptionName = newOption, OptionDesc = "", OptionFiles = new Dictionary< RelPath, HashSet< GamePath > >() } );
|
||||
_selector.SaveCurrentMod();
|
||||
if( newName.Length == 0 )
|
||||
{
|
||||
Penumbra.ModManager.RemoveModOption( i, group, Mod.Data );
|
||||
}
|
||||
else if( newName != opt.OptionName )
|
||||
{
|
||||
group.Options[ i ] = new Option()
|
||||
{ OptionName = newName, OptionDesc = opt.OptionDesc, OptionFiles = opt.OptionFiles };
|
||||
_selector.SaveCurrentMod();
|
||||
}
|
||||
|
||||
if( Mod!.Data.Meta.RefreshHasGroupsWithConfig() )
|
||||
{
|
||||
_selector.Cache.TriggerFilterReset();
|
||||
|
|
@ -140,244 +183,201 @@ namespace Penumbra.UI
|
|||
}
|
||||
}
|
||||
|
||||
private void DrawMultiSelectorEdit( OptionGroup group )
|
||||
DrawMultiSelectorEditAdd( group, nameBoxStart );
|
||||
}
|
||||
|
||||
private void DrawSingleSelectorEditGroup( OptionGroup group )
|
||||
{
|
||||
var groupName = group.GroupName;
|
||||
if( ImGui.InputText( $"##{groupName}_add", ref groupName, 64, ImGuiInputTextFlags.EnterReturnsTrue ) )
|
||||
{
|
||||
var nameBoxStart = CheckMarkSize;
|
||||
var flag = Mod!.Settings.Settings[ group.GroupName ];
|
||||
|
||||
using var raii = DrawMultiSelectorEditBegin( group );
|
||||
for( var i = 0; i < group.Options.Count; ++i )
|
||||
if( Penumbra.ModManager.ChangeModGroup( group.GroupName, groupName, Mod.Data ) && Mod.Data.Meta.RefreshHasGroupsWithConfig() )
|
||||
{
|
||||
var opt = group.Options[ i ];
|
||||
var label = $"##{group.GroupName}_{i}";
|
||||
DrawMultiSelectorCheckBox( group, i, flag, label );
|
||||
|
||||
ImGui.SameLine();
|
||||
var newName = opt.OptionName;
|
||||
|
||||
if( nameBoxStart == CheckMarkSize )
|
||||
{
|
||||
nameBoxStart = ImGui.GetCursorPosX();
|
||||
}
|
||||
|
||||
ImGui.SetNextItemWidth( MultiEditBoxWidth * ImGuiHelpers.GlobalScale );
|
||||
if( ImGui.InputText( $"{label}_l", ref newName, 64, ImGuiInputTextFlags.EnterReturnsTrue ) )
|
||||
{
|
||||
if( newName.Length == 0 )
|
||||
{
|
||||
Penumbra.ModManager.RemoveModOption( i, group, Mod.Data );
|
||||
}
|
||||
else if( newName != opt.OptionName )
|
||||
{
|
||||
group.Options[ i ] = new Option()
|
||||
{ OptionName = newName, OptionDesc = opt.OptionDesc, OptionFiles = opt.OptionFiles };
|
||||
_selector.SaveCurrentMod();
|
||||
}
|
||||
|
||||
if( Mod!.Data.Meta.RefreshHasGroupsWithConfig() )
|
||||
{
|
||||
_selector.Cache.TriggerFilterReset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DrawMultiSelectorEditAdd( group, nameBoxStart );
|
||||
}
|
||||
|
||||
private void DrawSingleSelectorEditGroup( OptionGroup group )
|
||||
{
|
||||
var groupName = group.GroupName;
|
||||
if( ImGui.InputText( $"##{groupName}_add", ref groupName, 64, ImGuiInputTextFlags.EnterReturnsTrue ) )
|
||||
{
|
||||
if( Penumbra.ModManager.ChangeModGroup( group.GroupName, groupName, Mod.Data ) && Mod.Data.Meta.RefreshHasGroupsWithConfig() )
|
||||
{
|
||||
_selector.Cache.TriggerFilterReset();
|
||||
}
|
||||
_selector.Cache.TriggerFilterReset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private float DrawSingleSelectorEdit( OptionGroup group )
|
||||
private float DrawSingleSelectorEdit( OptionGroup group )
|
||||
{
|
||||
var oldSetting = Mod!.Settings.Settings[ group.GroupName ];
|
||||
var code = oldSetting;
|
||||
if( ImGuiCustom.RenameableCombo( $"##{group.GroupName}", ref code, out var newName,
|
||||
group.Options.Select( x => x.OptionName ).ToArray(), group.Options.Count ) )
|
||||
{
|
||||
var oldSetting = Mod!.Settings.Settings[ group.GroupName ];
|
||||
var code = oldSetting;
|
||||
if( ImGuiCustom.RenameableCombo( $"##{group.GroupName}", ref code, out var newName,
|
||||
group.Options.Select( x => x.OptionName ).ToArray(), group.Options.Count ) )
|
||||
if( code == group.Options.Count )
|
||||
{
|
||||
if( code == group.Options.Count )
|
||||
if( newName.Length > 0 )
|
||||
{
|
||||
if( newName.Length > 0 )
|
||||
Mod.Settings.Settings[ group.GroupName ] = code;
|
||||
group.Options.Add( new Option()
|
||||
{
|
||||
Mod.Settings.Settings[ group.GroupName ] = code;
|
||||
group.Options.Add( new Option()
|
||||
{
|
||||
OptionName = newName,
|
||||
OptionDesc = "",
|
||||
OptionFiles = new Dictionary< RelPath, HashSet< GamePath > >(),
|
||||
} );
|
||||
_selector.SaveCurrentMod();
|
||||
}
|
||||
OptionName = newName,
|
||||
OptionDesc = "",
|
||||
OptionFiles = new Dictionary< Utf8RelPath, HashSet< Utf8GamePath > >(),
|
||||
} );
|
||||
_selector.SaveCurrentMod();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if( newName.Length == 0 )
|
||||
{
|
||||
Penumbra.ModManager.RemoveModOption( code, group, Mod.Data );
|
||||
}
|
||||
else
|
||||
{
|
||||
if( newName.Length == 0 )
|
||||
if( newName != group.Options[ code ].OptionName )
|
||||
{
|
||||
Penumbra.ModManager.RemoveModOption( code, group, Mod.Data );
|
||||
}
|
||||
else
|
||||
{
|
||||
if( newName != group.Options[ code ].OptionName )
|
||||
group.Options[ code ] = new Option()
|
||||
{
|
||||
group.Options[ code ] = new Option()
|
||||
{
|
||||
OptionName = newName, OptionDesc = group.Options[ code ].OptionDesc,
|
||||
OptionFiles = group.Options[ code ].OptionFiles,
|
||||
};
|
||||
_selector.SaveCurrentMod();
|
||||
}
|
||||
OptionName = newName, OptionDesc = group.Options[ code ].OptionDesc,
|
||||
OptionFiles = group.Options[ code ].OptionFiles,
|
||||
};
|
||||
_selector.SaveCurrentMod();
|
||||
}
|
||||
}
|
||||
|
||||
if( Mod.Data.Meta.RefreshHasGroupsWithConfig() )
|
||||
{
|
||||
_selector.Cache.TriggerFilterReset();
|
||||
}
|
||||
}
|
||||
|
||||
if( code != oldSetting )
|
||||
if( Mod.Data.Meta.RefreshHasGroupsWithConfig() )
|
||||
{
|
||||
Save();
|
||||
_selector.Cache.TriggerFilterReset();
|
||||
}
|
||||
}
|
||||
|
||||
if( code != oldSetting )
|
||||
{
|
||||
Save();
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
var labelEditPos = ImGui.GetCursorPosX();
|
||||
DrawSingleSelectorEditGroup( group );
|
||||
|
||||
return labelEditPos;
|
||||
}
|
||||
|
||||
private void DrawAddSingleGroupField( float labelEditPos )
|
||||
{
|
||||
var newGroup = "";
|
||||
ImGui.SetCursorPosX( labelEditPos );
|
||||
if( labelEditPos == CheckMarkSize )
|
||||
{
|
||||
ImGui.SetNextItemWidth( MultiEditBoxWidth * ImGuiHelpers.GlobalScale );
|
||||
}
|
||||
|
||||
if( ImGui.InputTextWithHint( LabelNewSingleGroupEdit, "Add new Single Group...", ref newGroup, 64,
|
||||
ImGuiInputTextFlags.EnterReturnsTrue ) )
|
||||
{
|
||||
Penumbra.ModManager.ChangeModGroup( "", newGroup, Mod.Data, SelectType.Single );
|
||||
// Adds empty group, so can not change filters.
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawAddMultiGroupField()
|
||||
{
|
||||
var newGroup = "";
|
||||
ImGui.SetCursorPosX( CheckMarkSize );
|
||||
ImGui.SetNextItemWidth( MultiEditBoxWidth * ImGuiHelpers.GlobalScale );
|
||||
if( ImGui.InputTextWithHint( LabelNewMultiGroup, "Add new Multi Group...", ref newGroup, 64,
|
||||
ImGuiInputTextFlags.EnterReturnsTrue ) )
|
||||
{
|
||||
Penumbra.ModManager.ChangeModGroup( "", newGroup, Mod.Data, SelectType.Multi );
|
||||
// Adds empty group, so can not change filters.
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawGroupSelectorsEdit()
|
||||
{
|
||||
var labelEditPos = CheckMarkSize;
|
||||
var groups = Meta.Groups.Values.ToArray();
|
||||
foreach( var g in groups.Where( g => g.SelectionType == SelectType.Single ) )
|
||||
{
|
||||
labelEditPos = DrawSingleSelectorEdit( g );
|
||||
}
|
||||
|
||||
DrawAddSingleGroupField( labelEditPos );
|
||||
|
||||
foreach( var g in groups.Where( g => g.SelectionType == SelectType.Multi ) )
|
||||
{
|
||||
DrawMultiSelectorEdit( g );
|
||||
}
|
||||
|
||||
DrawAddMultiGroupField();
|
||||
}
|
||||
|
||||
private void DrawFileSwapTabEdit()
|
||||
{
|
||||
if( !ImGui.BeginTabItem( LabelFileSwapTab ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem );
|
||||
|
||||
ImGui.SetNextItemWidth( -1 );
|
||||
if( !ImGui.BeginListBox( LabelFileSwapHeader, AutoFillSize ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
raii.Push( ImGui.EndListBox );
|
||||
|
||||
var swaps = Meta.FileSwaps.Keys.ToArray();
|
||||
|
||||
ImGui.PushFont( UiBuilder.IconFont );
|
||||
var arrowWidth = ImGui.CalcTextSize( FontAwesomeIcon.LongArrowAltRight.ToIconString() ).X;
|
||||
ImGui.PopFont();
|
||||
|
||||
var width = ( ImGui.GetWindowWidth() - arrowWidth - 4 * ImGui.GetStyle().ItemSpacing.X ) / 2;
|
||||
for( var idx = 0; idx < swaps.Length + 1; ++idx )
|
||||
{
|
||||
var key = idx == swaps.Length ? Utf8GamePath.Empty : swaps[ idx ];
|
||||
var value = idx == swaps.Length ? FullPath.Empty : Meta.FileSwaps[ key ];
|
||||
var keyString = key.ToString();
|
||||
var valueString = value.ToString();
|
||||
|
||||
ImGui.SetNextItemWidth( width );
|
||||
if( ImGui.InputTextWithHint( $"##swapLhs_{idx}", "Enter new file to be replaced...", ref keyString,
|
||||
GamePath.MaxGamePathLength, ImGuiInputTextFlags.EnterReturnsTrue ) )
|
||||
{
|
||||
if( Utf8GamePath.FromString( keyString, out var newKey, true ) && newKey.CompareTo( key ) != 0 )
|
||||
{
|
||||
if( idx < swaps.Length )
|
||||
{
|
||||
Meta.FileSwaps.Remove( key );
|
||||
}
|
||||
|
||||
if( !newKey.IsEmpty )
|
||||
{
|
||||
Meta.FileSwaps[ newKey ] = value;
|
||||
}
|
||||
|
||||
_selector.SaveCurrentMod();
|
||||
_selector.ReloadCurrentMod();
|
||||
}
|
||||
}
|
||||
|
||||
if( idx >= swaps.Length )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
var labelEditPos = ImGui.GetCursorPosX();
|
||||
DrawSingleSelectorEditGroup( group );
|
||||
ImGuiCustom.PrintIcon( FontAwesomeIcon.LongArrowAltRight );
|
||||
ImGui.SameLine();
|
||||
|
||||
return labelEditPos;
|
||||
}
|
||||
|
||||
private void DrawAddSingleGroupField( float labelEditPos )
|
||||
{
|
||||
var newGroup = "";
|
||||
ImGui.SetCursorPosX( labelEditPos );
|
||||
if( labelEditPos == CheckMarkSize )
|
||||
ImGui.SetNextItemWidth( width );
|
||||
if( ImGui.InputTextWithHint( $"##swapRhs_{idx}", "Enter new replacement path...", ref valueString,
|
||||
GamePath.MaxGamePathLength,
|
||||
ImGuiInputTextFlags.EnterReturnsTrue ) )
|
||||
{
|
||||
ImGui.SetNextItemWidth( MultiEditBoxWidth * ImGuiHelpers.GlobalScale );
|
||||
}
|
||||
|
||||
if( ImGui.InputTextWithHint( LabelNewSingleGroupEdit, "Add new Single Group...", ref newGroup, 64,
|
||||
ImGuiInputTextFlags.EnterReturnsTrue ) )
|
||||
{
|
||||
Penumbra.ModManager.ChangeModGroup( "", newGroup, Mod.Data, SelectType.Single );
|
||||
// Adds empty group, so can not change filters.
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawAddMultiGroupField()
|
||||
{
|
||||
var newGroup = "";
|
||||
ImGui.SetCursorPosX( CheckMarkSize );
|
||||
ImGui.SetNextItemWidth( MultiEditBoxWidth * ImGuiHelpers.GlobalScale );
|
||||
if( ImGui.InputTextWithHint( LabelNewMultiGroup, "Add new Multi Group...", ref newGroup, 64,
|
||||
ImGuiInputTextFlags.EnterReturnsTrue ) )
|
||||
{
|
||||
Penumbra.ModManager.ChangeModGroup( "", newGroup, Mod.Data, SelectType.Multi );
|
||||
// Adds empty group, so can not change filters.
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawGroupSelectorsEdit()
|
||||
{
|
||||
var labelEditPos = CheckMarkSize;
|
||||
var groups = Meta.Groups.Values.ToArray();
|
||||
foreach( var g in groups.Where( g => g.SelectionType == SelectType.Single ) )
|
||||
{
|
||||
labelEditPos = DrawSingleSelectorEdit( g );
|
||||
}
|
||||
|
||||
DrawAddSingleGroupField( labelEditPos );
|
||||
|
||||
foreach( var g in groups.Where( g => g.SelectionType == SelectType.Multi ) )
|
||||
{
|
||||
DrawMultiSelectorEdit( g );
|
||||
}
|
||||
|
||||
DrawAddMultiGroupField();
|
||||
}
|
||||
|
||||
private void DrawFileSwapTabEdit()
|
||||
{
|
||||
if( !ImGui.BeginTabItem( LabelFileSwapTab ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTabItem );
|
||||
|
||||
ImGui.SetNextItemWidth( -1 );
|
||||
if( !ImGui.BeginListBox( LabelFileSwapHeader, AutoFillSize ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
raii.Push( ImGui.EndListBox );
|
||||
|
||||
var swaps = Meta.FileSwaps.Keys.ToArray();
|
||||
|
||||
ImGui.PushFont( UiBuilder.IconFont );
|
||||
var arrowWidth = ImGui.CalcTextSize( FontAwesomeIcon.LongArrowAltRight.ToIconString() ).X;
|
||||
ImGui.PopFont();
|
||||
|
||||
var width = ( ImGui.GetWindowWidth() - arrowWidth - 4 * ImGui.GetStyle().ItemSpacing.X ) / 2;
|
||||
for( var idx = 0; idx < swaps.Length + 1; ++idx )
|
||||
{
|
||||
var key = idx == swaps.Length ? GamePath.GenerateUnchecked( "" ) : swaps[ idx ];
|
||||
var value = idx == swaps.Length ? GamePath.GenerateUnchecked( "" ) : Meta.FileSwaps[ key ];
|
||||
string keyString = key;
|
||||
string valueString = value;
|
||||
|
||||
ImGui.SetNextItemWidth( width );
|
||||
if( ImGui.InputTextWithHint( $"##swapLhs_{idx}", "Enter new file to be replaced...", ref keyString,
|
||||
GamePath.MaxGamePathLength, ImGuiInputTextFlags.EnterReturnsTrue ) )
|
||||
var newValue = new FullPath( valueString.ToLowerInvariant() );
|
||||
if( newValue.CompareTo( value ) != 0 )
|
||||
{
|
||||
var newKey = new GamePath( keyString );
|
||||
if( newKey.CompareTo( key ) != 0 )
|
||||
{
|
||||
if( idx < swaps.Length )
|
||||
{
|
||||
Meta.FileSwaps.Remove( key );
|
||||
}
|
||||
|
||||
if( newKey != string.Empty )
|
||||
{
|
||||
Meta.FileSwaps[ newKey ] = value;
|
||||
}
|
||||
|
||||
_selector.SaveCurrentMod();
|
||||
_selector.ReloadCurrentMod();
|
||||
}
|
||||
}
|
||||
|
||||
if( idx >= swaps.Length )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGuiCustom.PrintIcon( FontAwesomeIcon.LongArrowAltRight );
|
||||
ImGui.SameLine();
|
||||
|
||||
ImGui.SetNextItemWidth( width );
|
||||
if( ImGui.InputTextWithHint( $"##swapRhs_{idx}", "Enter new replacement path...", ref valueString,
|
||||
GamePath.MaxGamePathLength,
|
||||
ImGuiInputTextFlags.EnterReturnsTrue ) )
|
||||
{
|
||||
var newValue = new GamePath( valueString );
|
||||
if( newValue.CompareTo( value ) != 0 )
|
||||
{
|
||||
Meta.FileSwaps[ key ] = newValue;
|
||||
_selector.SaveCurrentMod();
|
||||
_selector.Cache.TriggerListReset();
|
||||
}
|
||||
Meta.FileSwaps[ key ] = newValue;
|
||||
_selector.SaveCurrentMod();
|
||||
_selector.Cache.TriggerListReset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -488,7 +488,7 @@ public partial class SettingsInterface
|
|||
|
||||
using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup );
|
||||
|
||||
if( ModPanel.DrawSortOrder( mod.Data, _modManager, this ) )
|
||||
if( ModPanel.DrawSortOrder( mod.Data, Penumbra.ModManager, this ) )
|
||||
{
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
|
|
@ -509,7 +509,7 @@ public partial class SettingsInterface
|
|||
{
|
||||
var change = false;
|
||||
var metaManips = false;
|
||||
foreach( var _ in folder.AllMods( _modManager.Config.SortFoldersFirst ) )
|
||||
foreach( var _ in folder.AllMods( Penumbra.ModManager.Config.SortFoldersFirst ) )
|
||||
{
|
||||
var (mod, _, _) = Cache.GetMod( currentIdx++ );
|
||||
if( mod != null )
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ using System.Diagnostics;
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Text.RegularExpressions;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Components;
|
||||
using Dalamud.Logging;
|
||||
using ImGuiNET;
|
||||
using Penumbra.GameData.ByteString;
|
||||
using Penumbra.Interop;
|
||||
|
|
@ -131,7 +133,7 @@ public partial class SettingsInterface
|
|||
|
||||
private void DrawEnabledBox()
|
||||
{
|
||||
var enabled = _config.IsEnabled;
|
||||
var enabled = _config.EnableMods;
|
||||
if( ImGui.Checkbox( "Enable Mods", ref enabled ) )
|
||||
{
|
||||
_base._penumbra.SetEnabled( enabled );
|
||||
|
|
@ -317,14 +319,84 @@ public partial class SettingsInterface
|
|||
+ "You usually should not need to do this." );
|
||||
}
|
||||
|
||||
private void DrawEnableFullResourceLoggingBox()
|
||||
{
|
||||
var tmp = _config.EnableFullResourceLogging;
|
||||
if( ImGui.Checkbox( "Enable Full Resource Logging", ref tmp ) && tmp != _config.EnableFullResourceLogging )
|
||||
{
|
||||
if( tmp )
|
||||
{
|
||||
_base._penumbra.ResourceLoader.EnableFullLogging();
|
||||
}
|
||||
else
|
||||
{
|
||||
_base._penumbra.ResourceLoader.DisableFullLogging();
|
||||
}
|
||||
|
||||
_config.EnableFullResourceLogging = tmp;
|
||||
_configChanged = true;
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGuiComponents.HelpMarker( "[DEBUG] Enable the logging of all ResourceLoader events indiscriminately." );
|
||||
}
|
||||
|
||||
private void DrawEnableDebugModeBox()
|
||||
{
|
||||
var tmp = _config.DebugMode;
|
||||
if( ImGui.Checkbox( "Enable Debug Mode", ref tmp ) && tmp != _config.DebugMode )
|
||||
{
|
||||
if( tmp )
|
||||
{
|
||||
_base._penumbra.ResourceLoader.EnableDebug();
|
||||
}
|
||||
else
|
||||
{
|
||||
_base._penumbra.ResourceLoader.DisableDebug();
|
||||
}
|
||||
|
||||
_config.DebugMode = tmp;
|
||||
_configChanged = true;
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGuiComponents.HelpMarker( "[DEBUG] Enable the Debug Tab and Resource Manager Tab as well as some additional data collection." );
|
||||
}
|
||||
|
||||
private void DrawRequestedResourceLogging()
|
||||
{
|
||||
var tmp = _config.EnableResourceLogging;
|
||||
if( ImGui.Checkbox( "Enable Requested Resource Logging", ref tmp ) )
|
||||
{
|
||||
_base._penumbra.ResourceLogger.SetState( tmp );
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGuiComponents.HelpMarker( "Log all game paths FFXIV requests to the plugin log.\n"
|
||||
+ "You can filter the logged paths for those containing the entered string or matching the regex, if the entered string compiles to a valid regex.\n"
|
||||
+ "Red boundary indicates invalid regex." );
|
||||
ImGui.SameLine();
|
||||
var tmpString = Penumbra.Config.ResourceLoggingFilter;
|
||||
using var color = ImGuiRaii.PushColor( ImGuiCol.Border, 0xFF0000B0, !_base._penumbra.ResourceLogger.ValidRegex );
|
||||
using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale,
|
||||
!_base._penumbra.ResourceLogger.ValidRegex );
|
||||
if( ImGui.InputTextWithHint( "##ResourceLogFilter", "Filter...", ref tmpString, Utf8GamePath.MaxGamePathLength ) )
|
||||
{
|
||||
_base._penumbra.ResourceLogger.SetFilter( tmpString );
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawAdvancedSettings()
|
||||
{
|
||||
DrawTempFolder();
|
||||
DrawRequestedResourceLogging();
|
||||
DrawDisableSoundStreamingBox();
|
||||
DrawLogLoadedFilesBox();
|
||||
DrawDisableNotificationsBox();
|
||||
DrawEnableHttpApiBox();
|
||||
DrawReloadResourceButton();
|
||||
DrawEnableDebugModeBox();
|
||||
DrawEnableFullResourceLoggingBox();
|
||||
}
|
||||
|
||||
public static unsafe void Text( Utf8String s )
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
using System.Numerics;
|
||||
using Dalamud.Interface;
|
||||
using ImGuiNET;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.UI.Custom;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.UI;
|
||||
|
||||
|
|
|
|||
|
|
@ -5,36 +5,35 @@ using Lumina.Excel.GeneratedSheets;
|
|||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.UI.Custom;
|
||||
|
||||
namespace Penumbra.UI
|
||||
namespace Penumbra.UI;
|
||||
|
||||
public partial class SettingsInterface
|
||||
{
|
||||
public partial class SettingsInterface
|
||||
internal void DrawChangedItem( string name, object? data, float itemIdOffset = 0 )
|
||||
{
|
||||
internal void DrawChangedItem( string name, object? data, float itemIdOffset = 0)
|
||||
var ret = ImGui.Selectable( name ) ? MouseButton.Left : MouseButton.None;
|
||||
ret = ImGui.IsItemClicked( ImGuiMouseButton.Right ) ? MouseButton.Right : ret;
|
||||
ret = ImGui.IsItemClicked( ImGuiMouseButton.Middle ) ? MouseButton.Middle : ret;
|
||||
|
||||
if( ret != MouseButton.None )
|
||||
{
|
||||
var ret = ImGui.Selectable( name ) ? MouseButton.Left : MouseButton.None;
|
||||
ret = ImGui.IsItemClicked( ImGuiMouseButton.Right ) ? MouseButton.Right : ret;
|
||||
ret = ImGui.IsItemClicked( ImGuiMouseButton.Middle ) ? MouseButton.Middle : ret;
|
||||
_penumbra.Api.InvokeClick( ret, data );
|
||||
}
|
||||
|
||||
if( ret != MouseButton.None )
|
||||
{
|
||||
_penumbra.Api.InvokeClick( ret, data );
|
||||
}
|
||||
if( _penumbra.Api.HasTooltip && ImGui.IsItemHovered() )
|
||||
{
|
||||
ImGui.BeginTooltip();
|
||||
using var tooltip = ImGuiRaii.DeferredEnd( ImGui.EndTooltip );
|
||||
_penumbra.Api.InvokeTooltip( data );
|
||||
}
|
||||
|
||||
if( _penumbra.Api.HasTooltip && ImGui.IsItemHovered() )
|
||||
{
|
||||
ImGui.BeginTooltip();
|
||||
using var tooltip = ImGuiRaii.DeferredEnd( ImGui.EndTooltip );
|
||||
_penumbra.Api.InvokeTooltip( data );
|
||||
}
|
||||
if( data is Item it )
|
||||
{
|
||||
var modelId = $"({( ( Quad )it.ModelMain ).A})";
|
||||
var offset = ImGui.CalcTextSize( modelId ).X - ImGui.GetStyle().ItemInnerSpacing.X + itemIdOffset;
|
||||
|
||||
if( data is Item it )
|
||||
{
|
||||
var modelId = $"({( ( Quad )it.ModelMain ).A})";
|
||||
var offset = ImGui.CalcTextSize( modelId ).X - ImGui.GetStyle().ItemInnerSpacing.X + itemIdOffset;
|
||||
|
||||
ImGui.SameLine( ImGui.GetWindowContentRegionWidth() - offset );
|
||||
ImGui.TextColored( new Vector4( 0.5f, 0.5f, 0.5f, 1 ), modelId );
|
||||
}
|
||||
ImGui.SameLine( ImGui.GetWindowContentRegionWidth() - offset );
|
||||
ImGui.TextColored( new Vector4( 0.5f, 0.5f, 0.5f, 1 ), modelId );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,87 +1,33 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Penumbra.Util
|
||||
namespace Penumbra.Util;
|
||||
|
||||
public static class ArrayExtensions
|
||||
{
|
||||
public static class ArrayExtensions
|
||||
public static int IndexOf< T >( this T[] array, Predicate< T > match )
|
||||
{
|
||||
public static T[] Slice< T >( this T[] source, int index, int length )
|
||||
for( var i = 0; i < array.Length; ++i )
|
||||
{
|
||||
var slice = new T[length];
|
||||
Array.Copy( source, index * length, slice, 0, length );
|
||||
return slice;
|
||||
}
|
||||
|
||||
public static void Swap< T >( this T[] array, int idx1, int idx2 )
|
||||
{
|
||||
var tmp = array[ idx1 ];
|
||||
array[ idx1 ] = array[ idx2 ];
|
||||
array[ idx2 ] = tmp;
|
||||
}
|
||||
|
||||
public static void Swap< T >( this List< T > array, int idx1, int idx2 )
|
||||
{
|
||||
var tmp = array[ idx1 ];
|
||||
array[ idx1 ] = array[ idx2 ];
|
||||
array[ idx2 ] = tmp;
|
||||
}
|
||||
|
||||
public static int IndexOf< T >( this T[] array, Predicate< T > match )
|
||||
{
|
||||
for( var i = 0; i < array.Length; ++i )
|
||||
if( match( array[ i ] ) )
|
||||
{
|
||||
if( match( array[ i ] ) )
|
||||
{
|
||||
return i;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
public static void Swap< T >( this T[] array, T lhs, T rhs )
|
||||
return -1;
|
||||
}
|
||||
|
||||
public static int IndexOf< T >( this IList< T > array, Func< T, bool > predicate )
|
||||
{
|
||||
for( var i = 0; i < array.Count; ++i )
|
||||
{
|
||||
var idx1 = Array.IndexOf( array, lhs );
|
||||
if( idx1 < 0 )
|
||||
if( predicate.Invoke( array[ i ] ) )
|
||||
{
|
||||
return;
|
||||
return i;
|
||||
}
|
||||
|
||||
var idx2 = Array.IndexOf( array, rhs );
|
||||
if( idx2 < 0 )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
array.Swap( idx1, idx2 );
|
||||
}
|
||||
|
||||
public static void Swap< T >( this List< T > array, T lhs, T rhs )
|
||||
{
|
||||
var idx1 = array.IndexOf( lhs );
|
||||
if( idx1 < 0 )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var idx2 = array.IndexOf( rhs );
|
||||
if( idx2 < 0 )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
array.Swap( idx1, idx2 );
|
||||
}
|
||||
|
||||
public static int IndexOf< T >( this IList< T > array, Func< T, bool > predicate )
|
||||
{
|
||||
for( var i = 0; i < array.Count; ++i )
|
||||
{
|
||||
if( predicate.Invoke( array[ i ] ) )
|
||||
return i;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,135 +3,54 @@ using System.Collections.Generic;
|
|||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Penumbra.Util
|
||||
namespace Penumbra.Util;
|
||||
|
||||
public static class BinaryReaderExtensions
|
||||
{
|
||||
public static class BinaryReaderExtensions
|
||||
/// <summary>
|
||||
/// Reads a structure from the current stream position.
|
||||
/// </summary>
|
||||
/// <param name="br"></param>
|
||||
/// <typeparam name="T">The structure to read in to</typeparam>
|
||||
/// <returns>The file data as a structure</returns>
|
||||
public static T ReadStructure< T >( this BinaryReader br ) where T : struct
|
||||
{
|
||||
/// <summary>
|
||||
/// Reads a structure from the current stream position.
|
||||
/// </summary>
|
||||
/// <param name="br"></param>
|
||||
/// <typeparam name="T">The structure to read in to</typeparam>
|
||||
/// <returns>The file data as a structure</returns>
|
||||
public static T ReadStructure< T >( this BinaryReader br ) where T : struct
|
||||
{
|
||||
ReadOnlySpan< byte > data = br.ReadBytes( Unsafe.SizeOf< T >() );
|
||||
ReadOnlySpan< byte > data = br.ReadBytes( Unsafe.SizeOf< T >() );
|
||||
|
||||
return MemoryMarshal.Read< T >( data );
|
||||
return MemoryMarshal.Read< T >( data );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads many structures from the current stream position.
|
||||
/// </summary>
|
||||
/// <param name="br"></param>
|
||||
/// <param name="count">The number of T to read from the stream</param>
|
||||
/// <typeparam name="T">The structure to read in to</typeparam>
|
||||
/// <returns>A list containing the structures read from the stream</returns>
|
||||
public static List< T > ReadStructures< T >( this BinaryReader br, int count ) where T : struct
|
||||
{
|
||||
var size = Marshal.SizeOf< T >();
|
||||
var data = br.ReadBytes( size * count );
|
||||
|
||||
var list = new List< T >( count );
|
||||
|
||||
for( var i = 0; i < count; i++ )
|
||||
{
|
||||
var offset = size * i;
|
||||
var span = new ReadOnlySpan< byte >( data, offset, size );
|
||||
|
||||
list.Add( MemoryMarshal.Read< T >( span ) );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads many structures from the current stream position.
|
||||
/// </summary>
|
||||
/// <param name="br"></param>
|
||||
/// <param name="count">The number of T to read from the stream</param>
|
||||
/// <typeparam name="T">The structure to read in to</typeparam>
|
||||
/// <returns>A list containing the structures read from the stream</returns>
|
||||
public static List< T > ReadStructures< T >( this BinaryReader br, int count ) where T : struct
|
||||
{
|
||||
var size = Marshal.SizeOf< T >();
|
||||
var data = br.ReadBytes( size * count );
|
||||
return list;
|
||||
}
|
||||
|
||||
var list = new List< T >( count );
|
||||
|
||||
for( var i = 0; i < count; i++ )
|
||||
{
|
||||
var offset = size * i;
|
||||
var span = new ReadOnlySpan< byte >( data, offset, size );
|
||||
|
||||
list.Add( MemoryMarshal.Read< T >( span ) );
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public static T[] ReadStructuresAsArray< T >( this BinaryReader br, int count ) where T : struct
|
||||
{
|
||||
var size = Marshal.SizeOf< T >();
|
||||
var data = br.ReadBytes( size * count );
|
||||
|
||||
// im a pirate arr
|
||||
var arr = new T[count];
|
||||
|
||||
for( var i = 0; i < count; i++ )
|
||||
{
|
||||
var offset = size * i;
|
||||
var span = new ReadOnlySpan< byte >( data, offset, size );
|
||||
|
||||
arr[ i ] = MemoryMarshal.Read< T >( span );
|
||||
}
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves the BinaryReader position to offset, reads a string, then
|
||||
/// sets the reader position back to where it was when it started
|
||||
/// </summary>
|
||||
/// <param name="br"></param>
|
||||
/// <param name="offset">The offset to read a string starting from.</param>
|
||||
/// <returns></returns>
|
||||
public static string ReadStringOffsetData( this BinaryReader br, long offset )
|
||||
=> Encoding.UTF8.GetString( ReadRawOffsetData( br, offset ) );
|
||||
|
||||
/// <summary>
|
||||
/// Moves the BinaryReader position to offset, reads raw bytes until a null byte, then
|
||||
/// sets the reader position back to where it was when it started
|
||||
/// </summary>
|
||||
/// <param name="br"></param>
|
||||
/// <param name="offset">The offset to read data starting from.</param>
|
||||
/// <returns></returns>
|
||||
public static byte[] ReadRawOffsetData( this BinaryReader br, long offset )
|
||||
{
|
||||
var originalPosition = br.BaseStream.Position;
|
||||
br.BaseStream.Position = offset;
|
||||
|
||||
var chars = new List< byte >();
|
||||
|
||||
byte current;
|
||||
while( ( current = br.ReadByte() ) != 0 )
|
||||
{
|
||||
chars.Add( current );
|
||||
}
|
||||
|
||||
br.BaseStream.Position = originalPosition;
|
||||
|
||||
return chars.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Seeks this BinaryReader's position to the given offset. Syntactic sugar.
|
||||
/// </summary>
|
||||
public static void Seek( this BinaryReader br, long offset )
|
||||
{
|
||||
br.BaseStream.Position = offset;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a byte and moves the stream position back to where it started before the operation
|
||||
/// </summary>
|
||||
/// <param name="br">The reader to use to read the byte</param>
|
||||
/// <returns>The byte that was read</returns>
|
||||
public static byte PeekByte( this BinaryReader br )
|
||||
{
|
||||
var data = br.ReadByte();
|
||||
br.BaseStream.Position--;
|
||||
return data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads bytes and moves the stream position back to where it started before the operation
|
||||
/// </summary>
|
||||
/// <param name="br">The reader to use to read the bytes</param>
|
||||
/// <param name="count">The number of bytes to read</param>
|
||||
/// <returns>The read bytes</returns>
|
||||
public static byte[] PeekBytes( this BinaryReader br, int count )
|
||||
{
|
||||
var data = br.ReadBytes( count );
|
||||
br.BaseStream.Position -= count;
|
||||
return data;
|
||||
}
|
||||
/// <summary>
|
||||
/// Seeks this BinaryReader's position to the given offset. Syntactic sugar.
|
||||
/// </summary>
|
||||
public static void Seek( this BinaryReader br, long offset )
|
||||
{
|
||||
br.BaseStream.Position = offset;
|
||||
}
|
||||
}
|
||||
|
|
@ -2,36 +2,34 @@ using System.Collections.Generic;
|
|||
using Dalamud.Game.Text;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||
using Dalamud.Plugin;
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
|
||||
namespace Penumbra.Util
|
||||
namespace Penumbra.Util;
|
||||
|
||||
public static class ChatUtil
|
||||
{
|
||||
public static class ChatUtil
|
||||
public static void LinkItem( Item item )
|
||||
{
|
||||
public static void LinkItem( Item item )
|
||||
var payloadList = new List< Payload >
|
||||
{
|
||||
var payloadList = new List< Payload >
|
||||
{
|
||||
new UIForegroundPayload( ( ushort )( 0x223 + item.Rarity * 2 ) ),
|
||||
new UIGlowPayload( ( ushort )( 0x224 + item.Rarity * 2 ) ),
|
||||
new ItemPayload( item.RowId, false ),
|
||||
new UIForegroundPayload( 500 ),
|
||||
new UIGlowPayload( 501 ),
|
||||
new TextPayload( $"{( char )SeIconChar.LinkMarker}" ),
|
||||
new UIForegroundPayload( 0 ),
|
||||
new UIGlowPayload( 0 ),
|
||||
new TextPayload( item.Name ),
|
||||
new RawPayload( new byte[] { 0x02, 0x27, 0x07, 0xCF, 0x01, 0x01, 0x01, 0xFF, 0x01, 0x03 } ),
|
||||
new RawPayload( new byte[] { 0x02, 0x13, 0x02, 0xEC, 0x03 } ),
|
||||
};
|
||||
new UIForegroundPayload( ( ushort )( 0x223 + item.Rarity * 2 ) ),
|
||||
new UIGlowPayload( ( ushort )( 0x224 + item.Rarity * 2 ) ),
|
||||
new ItemPayload( item.RowId, false ),
|
||||
new UIForegroundPayload( 500 ),
|
||||
new UIGlowPayload( 501 ),
|
||||
new TextPayload( $"{( char )SeIconChar.LinkMarker}" ),
|
||||
new UIForegroundPayload( 0 ),
|
||||
new UIGlowPayload( 0 ),
|
||||
new TextPayload( item.Name ),
|
||||
new RawPayload( new byte[] { 0x02, 0x27, 0x07, 0xCF, 0x01, 0x01, 0x01, 0xFF, 0x01, 0x03 } ),
|
||||
new RawPayload( new byte[] { 0x02, 0x13, 0x02, 0xEC, 0x03 } ),
|
||||
};
|
||||
|
||||
var payload = new SeString( payloadList );
|
||||
var payload = new SeString( payloadList );
|
||||
|
||||
Dalamud.Chat.PrintChat( new XivChatEntry
|
||||
{
|
||||
Message = payload,
|
||||
} );
|
||||
}
|
||||
Dalamud.Chat.PrintChat( new XivChatEntry
|
||||
{
|
||||
Message = payload,
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Penumbra.Util
|
||||
{
|
||||
/// <summary>
|
||||
/// Performs the 32-bit reversed variant of the cyclic redundancy check algorithm
|
||||
/// </summary>
|
||||
public class Crc32
|
||||
{
|
||||
private const uint Poly = 0xedb88320;
|
||||
|
||||
private static readonly uint[] CrcArray =
|
||||
Enumerable.Range( 0, 256 ).Select( i =>
|
||||
{
|
||||
var k = ( uint )i;
|
||||
for( var j = 0; j < 8; j++ )
|
||||
{
|
||||
k = ( k & 1 ) != 0 ? ( k >> 1 ) ^ Poly : k >> 1;
|
||||
}
|
||||
|
||||
return k;
|
||||
} ).ToArray();
|
||||
|
||||
public uint Checksum
|
||||
=> ~_crc32;
|
||||
|
||||
private uint _crc32 = 0xFFFFFFFF;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes Crc32's state
|
||||
/// </summary>
|
||||
public void Init()
|
||||
{
|
||||
_crc32 = 0xFFFFFFFF;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates Crc32's state with new data
|
||||
/// </summary>
|
||||
/// <param name="data">Data to calculate the new CRC from</param>
|
||||
[MethodImpl( MethodImplOptions.AggressiveInlining )]
|
||||
public void Update( byte[] data )
|
||||
{
|
||||
foreach( var b in data )
|
||||
{
|
||||
Update( b );
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl( MethodImplOptions.AggressiveInlining )]
|
||||
public void Update( byte b )
|
||||
{
|
||||
_crc32 = CrcArray[ ( _crc32 ^ b ) & 0xFF ] ^ ( ( _crc32 >> 8 ) & 0x00FFFFFF );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,78 +5,77 @@ using System.Threading;
|
|||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Penumbra.Util
|
||||
namespace Penumbra.Util;
|
||||
|
||||
public static class DialogExtensions
|
||||
{
|
||||
public static class DialogExtensions
|
||||
public static Task< DialogResult > ShowDialogAsync( this CommonDialog form )
|
||||
{
|
||||
public static Task< DialogResult > ShowDialogAsync( this CommonDialog form )
|
||||
using var process = Process.GetCurrentProcess();
|
||||
return form.ShowDialogAsync( new DialogHandle( process.MainWindowHandle ) );
|
||||
}
|
||||
|
||||
public static Task< DialogResult > ShowDialogAsync( this CommonDialog form, IWin32Window owner )
|
||||
{
|
||||
var taskSource = new TaskCompletionSource< DialogResult >();
|
||||
var th = new Thread( () => DialogThread( form, owner, taskSource ) );
|
||||
th.Start();
|
||||
return taskSource.Task;
|
||||
}
|
||||
|
||||
[STAThread]
|
||||
private static void DialogThread( CommonDialog form, IWin32Window owner,
|
||||
TaskCompletionSource< DialogResult > taskSource )
|
||||
{
|
||||
Application.SetCompatibleTextRenderingDefault( false );
|
||||
Application.EnableVisualStyles();
|
||||
using var hiddenForm = new HiddenForm( form, owner, taskSource );
|
||||
Application.Run( hiddenForm );
|
||||
Application.ExitThread();
|
||||
}
|
||||
|
||||
public class DialogHandle : IWin32Window
|
||||
{
|
||||
public IntPtr Handle { get; set; }
|
||||
|
||||
public DialogHandle( IntPtr handle )
|
||||
=> Handle = handle;
|
||||
}
|
||||
|
||||
public class HiddenForm : Form
|
||||
{
|
||||
private readonly CommonDialog _form;
|
||||
private readonly IWin32Window _owner;
|
||||
private readonly TaskCompletionSource< DialogResult > _taskSource;
|
||||
|
||||
public HiddenForm( CommonDialog form, IWin32Window owner, TaskCompletionSource< DialogResult > taskSource )
|
||||
{
|
||||
using var process = Process.GetCurrentProcess();
|
||||
return form.ShowDialogAsync( new DialogHandle( process.MainWindowHandle ) );
|
||||
_form = form;
|
||||
_owner = owner;
|
||||
_taskSource = taskSource;
|
||||
|
||||
Opacity = 0;
|
||||
FormBorderStyle = FormBorderStyle.None;
|
||||
ShowInTaskbar = false;
|
||||
Size = new Size( 0, 0 );
|
||||
|
||||
Shown += HiddenForm_Shown;
|
||||
}
|
||||
|
||||
public static Task< DialogResult > ShowDialogAsync( this CommonDialog form, IWin32Window owner )
|
||||
private void HiddenForm_Shown( object? sender, EventArgs _ )
|
||||
{
|
||||
var taskSource = new TaskCompletionSource< DialogResult >();
|
||||
var th = new Thread( () => DialogThread( form, owner, taskSource ) );
|
||||
th.Start();
|
||||
return taskSource.Task;
|
||||
}
|
||||
|
||||
[STAThread]
|
||||
private static void DialogThread( CommonDialog form, IWin32Window owner,
|
||||
TaskCompletionSource< DialogResult > taskSource )
|
||||
{
|
||||
Application.SetCompatibleTextRenderingDefault( false );
|
||||
Application.EnableVisualStyles();
|
||||
using var hiddenForm = new HiddenForm( form, owner, taskSource );
|
||||
Application.Run( hiddenForm );
|
||||
Application.ExitThread();
|
||||
}
|
||||
|
||||
public class DialogHandle : IWin32Window
|
||||
{
|
||||
public IntPtr Handle { get; set; }
|
||||
|
||||
public DialogHandle( IntPtr handle )
|
||||
=> Handle = handle;
|
||||
}
|
||||
|
||||
public class HiddenForm : Form
|
||||
{
|
||||
private readonly CommonDialog _form;
|
||||
private readonly IWin32Window _owner;
|
||||
private readonly TaskCompletionSource< DialogResult > _taskSource;
|
||||
|
||||
public HiddenForm( CommonDialog form, IWin32Window owner, TaskCompletionSource< DialogResult > taskSource )
|
||||
Hide();
|
||||
try
|
||||
{
|
||||
_form = form;
|
||||
_owner = owner;
|
||||
_taskSource = taskSource;
|
||||
|
||||
Opacity = 0;
|
||||
FormBorderStyle = FormBorderStyle.None;
|
||||
ShowInTaskbar = false;
|
||||
Size = new Size( 0, 0 );
|
||||
|
||||
Shown += HiddenForm_Shown;
|
||||
var result = _form.ShowDialog( _owner );
|
||||
_taskSource.SetResult( result );
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
_taskSource.SetException( e );
|
||||
}
|
||||
|
||||
private void HiddenForm_Shown( object? sender, EventArgs _ )
|
||||
{
|
||||
Hide();
|
||||
try
|
||||
{
|
||||
var result = _form.ShowDialog( _owner );
|
||||
_taskSource.SetResult( result );
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
_taskSource.SetException( e );
|
||||
}
|
||||
|
||||
Close();
|
||||
}
|
||||
Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
using System;
|
||||
using Dalamud.Logging;
|
||||
|
||||
namespace Penumbra.Util
|
||||
{
|
||||
public static class GeneralUtil
|
||||
{
|
||||
public static void PrintDebugAddress( string name, IntPtr address )
|
||||
{
|
||||
var module = Dalamud.SigScanner.Module.BaseAddress.ToInt64();
|
||||
PluginLog.Debug( "{Name} found at 0x{Address:X16}, +0x{Offset:X}", name, address.ToInt64(), address.ToInt64() - module );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
using System.IO;
|
||||
|
||||
namespace Penumbra.Util
|
||||
{
|
||||
public static class MemoryStreamExtensions
|
||||
{
|
||||
public static void Write( this MemoryStream stream, byte[] data )
|
||||
{
|
||||
stream.Write( data, 0, data.Length );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -16,7 +16,6 @@ public static class ModelChanger
|
|||
public const string MaterialFormat = "/mt_c0201b0001_{0}.mtrl";
|
||||
public static readonly Regex MaterialRegex = new(@"/mt_c0201b0001_.*?\.mtrl", RegexOptions.Compiled);
|
||||
|
||||
|
||||
public static bool ValidStrings( string from, string to )
|
||||
=> from.Length != 0
|
||||
&& to.Length != 0
|
||||
|
|
@ -40,8 +39,8 @@ public static class ModelChanger
|
|||
|
||||
try
|
||||
{
|
||||
var data = File.ReadAllBytes( file.FullName );
|
||||
var mdlFile = new MdlFile( data );
|
||||
var data = File.ReadAllBytes( file.FullName );
|
||||
var mdlFile = new MdlFile( data );
|
||||
Func< string, bool > compare = MaterialRegex.IsMatch;
|
||||
if( from.Length > 0 )
|
||||
{
|
||||
|
|
@ -53,9 +52,9 @@ public static class ModelChanger
|
|||
var replaced = 0;
|
||||
for( var i = 0; i < mdlFile.Materials.Length; ++i )
|
||||
{
|
||||
if( compare(mdlFile.Materials[i]) )
|
||||
if( compare( mdlFile.Materials[ i ] ) )
|
||||
{
|
||||
mdlFile.Materials[i] = to;
|
||||
mdlFile.Materials[ i ] = to;
|
||||
++replaced;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,432 +3,405 @@ using System.Diagnostics;
|
|||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using Lumina;
|
||||
using Lumina.Data.Structs;
|
||||
|
||||
namespace Penumbra.Util
|
||||
namespace Penumbra.Util;
|
||||
|
||||
public class PenumbraSqPackStream : IDisposable
|
||||
{
|
||||
public class PenumbraSqPackStream : IDisposable
|
||||
public Stream BaseStream { get; protected set; }
|
||||
|
||||
protected BinaryReader Reader { get; set; }
|
||||
|
||||
public PenumbraSqPackStream( FileInfo file )
|
||||
: this( file.OpenRead() )
|
||||
{ }
|
||||
|
||||
public PenumbraSqPackStream( Stream stream )
|
||||
{
|
||||
public Stream BaseStream { get; protected set; }
|
||||
BaseStream = stream;
|
||||
Reader = new BinaryReader( BaseStream );
|
||||
}
|
||||
|
||||
protected BinaryReader Reader { get; set; }
|
||||
public SqPackHeader GetSqPackHeader()
|
||||
{
|
||||
BaseStream.Position = 0;
|
||||
|
||||
public PenumbraSqPackStream( FileInfo file )
|
||||
: this( file.OpenRead() )
|
||||
{ }
|
||||
return Reader.ReadStructure< SqPackHeader >();
|
||||
}
|
||||
|
||||
public PenumbraSqPackStream( Stream stream )
|
||||
{
|
||||
BaseStream = stream;
|
||||
Reader = new BinaryReader( BaseStream );
|
||||
}
|
||||
public SqPackFileInfo GetFileMetadata( long offset )
|
||||
{
|
||||
BaseStream.Position = offset;
|
||||
|
||||
public SqPackHeader GetSqPackHeader()
|
||||
{
|
||||
BaseStream.Position = 0;
|
||||
return Reader.ReadStructure< SqPackFileInfo >();
|
||||
}
|
||||
|
||||
return Reader.ReadStructure< SqPackHeader >();
|
||||
}
|
||||
public T ReadFile< T >( long offset ) where T : PenumbraFileResource
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
|
||||
public SqPackFileInfo GetFileMetadata( long offset )
|
||||
BaseStream.Position = offset;
|
||||
|
||||
var fileInfo = Reader.ReadStructure< SqPackFileInfo >();
|
||||
var file = Activator.CreateInstance< T >();
|
||||
|
||||
// check if we need to read the extended model header or just default to the standard file header
|
||||
if( fileInfo.Type == FileType.Model )
|
||||
{
|
||||
BaseStream.Position = offset;
|
||||
|
||||
return Reader.ReadStructure< SqPackFileInfo >();
|
||||
var modelFileInfo = Reader.ReadStructure< ModelBlock >();
|
||||
|
||||
file.FileInfo = new PenumbraFileInfo
|
||||
{
|
||||
HeaderSize = modelFileInfo.Size,
|
||||
Type = modelFileInfo.Type,
|
||||
BlockCount = modelFileInfo.UsedNumberOfBlocks,
|
||||
RawFileSize = modelFileInfo.RawFileSize,
|
||||
Offset = offset,
|
||||
|
||||
// todo: is this useful?
|
||||
ModelBlock = modelFileInfo,
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
file.FileInfo = new PenumbraFileInfo
|
||||
{
|
||||
HeaderSize = fileInfo.Size,
|
||||
Type = fileInfo.Type,
|
||||
BlockCount = fileInfo.NumberOfBlocks,
|
||||
RawFileSize = fileInfo.RawFileSize,
|
||||
Offset = offset,
|
||||
};
|
||||
}
|
||||
|
||||
public T ReadFile< T >( long offset ) where T : PenumbraFileResource
|
||||
switch( fileInfo.Type )
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
case FileType.Empty: throw new FileNotFoundException( $"The file located at 0x{offset:x} is empty." );
|
||||
|
||||
BaseStream.Position = offset;
|
||||
case FileType.Standard:
|
||||
ReadStandardFile( file, ms );
|
||||
break;
|
||||
|
||||
var fileInfo = Reader.ReadStructure< SqPackFileInfo >();
|
||||
var file = Activator.CreateInstance< T >();
|
||||
case FileType.Model:
|
||||
ReadModelFile( file, ms );
|
||||
break;
|
||||
|
||||
// check if we need to read the extended model header or just default to the standard file header
|
||||
if( fileInfo.Type == FileType.Model )
|
||||
{
|
||||
BaseStream.Position = offset;
|
||||
case FileType.Texture:
|
||||
ReadTextureFile( file, ms );
|
||||
break;
|
||||
|
||||
var modelFileInfo = Reader.ReadStructure< ModelBlock >();
|
||||
|
||||
file.FileInfo = new PenumbraFileInfo
|
||||
{
|
||||
HeaderSize = modelFileInfo.Size,
|
||||
Type = modelFileInfo.Type,
|
||||
BlockCount = modelFileInfo.UsedNumberOfBlocks,
|
||||
RawFileSize = modelFileInfo.RawFileSize,
|
||||
Offset = offset,
|
||||
|
||||
// todo: is this useful?
|
||||
ModelBlock = modelFileInfo,
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
file.FileInfo = new PenumbraFileInfo
|
||||
{
|
||||
HeaderSize = fileInfo.Size,
|
||||
Type = fileInfo.Type,
|
||||
BlockCount = fileInfo.NumberOfBlocks,
|
||||
RawFileSize = fileInfo.RawFileSize,
|
||||
Offset = offset,
|
||||
};
|
||||
}
|
||||
|
||||
switch( fileInfo.Type )
|
||||
{
|
||||
case FileType.Empty: throw new FileNotFoundException( $"The file located at 0x{offset:x} is empty." );
|
||||
|
||||
case FileType.Standard:
|
||||
ReadStandardFile( file, ms );
|
||||
break;
|
||||
|
||||
case FileType.Model:
|
||||
ReadModelFile( file, ms );
|
||||
break;
|
||||
|
||||
case FileType.Texture:
|
||||
ReadTextureFile( file, ms );
|
||||
break;
|
||||
|
||||
default: throw new NotImplementedException( $"File Type {( uint )fileInfo.Type} is not implemented." );
|
||||
}
|
||||
|
||||
file.Data = ms.ToArray();
|
||||
if( file.Data.Length != file.FileInfo.RawFileSize )
|
||||
{
|
||||
Debug.WriteLine( "Read data size does not match file size." );
|
||||
}
|
||||
|
||||
file.FileStream = new MemoryStream( file.Data, false );
|
||||
file.Reader = new BinaryReader( file.FileStream );
|
||||
file.FileStream.Position = 0;
|
||||
|
||||
file.LoadFile();
|
||||
|
||||
return file;
|
||||
default: throw new NotImplementedException( $"File Type {( uint )fileInfo.Type} is not implemented." );
|
||||
}
|
||||
|
||||
private void ReadStandardFile( PenumbraFileResource resource, MemoryStream ms )
|
||||
file.Data = ms.ToArray();
|
||||
if( file.Data.Length != file.FileInfo.RawFileSize )
|
||||
{
|
||||
var blocks = Reader.ReadStructures< DatStdFileBlockInfos >( ( int )resource.FileInfo!.BlockCount );
|
||||
|
||||
foreach( var block in blocks )
|
||||
{
|
||||
ReadFileBlock( resource.FileInfo.Offset + resource.FileInfo.HeaderSize + block.Offset, ms );
|
||||
}
|
||||
|
||||
// reset position ready for reading
|
||||
ms.Position = 0;
|
||||
Debug.WriteLine( "Read data size does not match file size." );
|
||||
}
|
||||
|
||||
private unsafe void ReadModelFile( PenumbraFileResource resource, MemoryStream ms )
|
||||
file.FileStream = new MemoryStream( file.Data, false );
|
||||
file.Reader = new BinaryReader( file.FileStream );
|
||||
file.FileStream.Position = 0;
|
||||
|
||||
file.LoadFile();
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
private void ReadStandardFile( PenumbraFileResource resource, MemoryStream ms )
|
||||
{
|
||||
var blocks = Reader.ReadStructures< DatStdFileBlockInfos >( ( int )resource.FileInfo!.BlockCount );
|
||||
|
||||
foreach( var block in blocks )
|
||||
{
|
||||
var mdlBlock = resource.FileInfo!.ModelBlock;
|
||||
var baseOffset = resource.FileInfo.Offset + resource.FileInfo.HeaderSize;
|
||||
ReadFileBlock( resource.FileInfo.Offset + resource.FileInfo.HeaderSize + block.Offset, ms );
|
||||
}
|
||||
|
||||
// 1/1/3/3/3 stack/runtime/vertex/egeo/index
|
||||
// TODO: consider testing if this is more reliable than the Explorer method
|
||||
// of adding mdlBlock.IndexBufferDataBlockIndex[2] + mdlBlock.IndexBufferDataBlockNum[2]
|
||||
// i don't want to move this to that method right now, because i know sometimes the index is 0
|
||||
// but it seems to work fine in explorer...
|
||||
int totalBlocks = mdlBlock.StackBlockNum;
|
||||
totalBlocks += mdlBlock.RuntimeBlockNum;
|
||||
for( var i = 0; i < 3; i++ )
|
||||
// reset position ready for reading
|
||||
ms.Position = 0;
|
||||
}
|
||||
|
||||
private unsafe void ReadModelFile( PenumbraFileResource resource, MemoryStream ms )
|
||||
{
|
||||
var mdlBlock = resource.FileInfo!.ModelBlock;
|
||||
var baseOffset = resource.FileInfo.Offset + resource.FileInfo.HeaderSize;
|
||||
|
||||
// 1/1/3/3/3 stack/runtime/vertex/egeo/index
|
||||
// TODO: consider testing if this is more reliable than the Explorer method
|
||||
// of adding mdlBlock.IndexBufferDataBlockIndex[2] + mdlBlock.IndexBufferDataBlockNum[2]
|
||||
// i don't want to move this to that method right now, because i know sometimes the index is 0
|
||||
// but it seems to work fine in explorer...
|
||||
int totalBlocks = mdlBlock.StackBlockNum;
|
||||
totalBlocks += mdlBlock.RuntimeBlockNum;
|
||||
for( var i = 0; i < 3; i++ )
|
||||
{
|
||||
totalBlocks += mdlBlock.VertexBufferBlockNum[ i ];
|
||||
}
|
||||
|
||||
for( var i = 0; i < 3; i++ )
|
||||
{
|
||||
totalBlocks += mdlBlock.EdgeGeometryVertexBufferBlockNum[ i ];
|
||||
}
|
||||
|
||||
for( var i = 0; i < 3; i++ )
|
||||
{
|
||||
totalBlocks += mdlBlock.IndexBufferBlockNum[ i ];
|
||||
}
|
||||
|
||||
var compressedBlockSizes = Reader.ReadStructures< ushort >( totalBlocks );
|
||||
var currentBlock = 0;
|
||||
var vertexDataOffsets = new int[3];
|
||||
var indexDataOffsets = new int[3];
|
||||
var vertexBufferSizes = new int[3];
|
||||
var indexBufferSizes = new int[3];
|
||||
|
||||
ms.Seek( 0x44, SeekOrigin.Begin );
|
||||
|
||||
Reader.Seek( baseOffset + mdlBlock.StackOffset );
|
||||
var stackStart = ms.Position;
|
||||
for( var i = 0; i < mdlBlock.StackBlockNum; i++ )
|
||||
{
|
||||
var lastPos = Reader.BaseStream.Position;
|
||||
ReadFileBlock( ms );
|
||||
Reader.Seek( lastPos + compressedBlockSizes[ currentBlock ] );
|
||||
currentBlock++;
|
||||
}
|
||||
|
||||
var stackEnd = ms.Position;
|
||||
var stackSize = ( int )( stackEnd - stackStart );
|
||||
|
||||
Reader.Seek( baseOffset + mdlBlock.RuntimeOffset );
|
||||
var runtimeStart = ms.Position;
|
||||
for( var i = 0; i < mdlBlock.RuntimeBlockNum; i++ )
|
||||
{
|
||||
var lastPos = Reader.BaseStream.Position;
|
||||
ReadFileBlock( ms );
|
||||
Reader.Seek( lastPos + compressedBlockSizes[ currentBlock ] );
|
||||
currentBlock++;
|
||||
}
|
||||
|
||||
var runtimeEnd = ms.Position;
|
||||
var runtimeSize = ( int )( runtimeEnd - runtimeStart );
|
||||
|
||||
for( var i = 0; i < 3; i++ )
|
||||
{
|
||||
if( mdlBlock.VertexBufferBlockNum[ i ] != 0 )
|
||||
{
|
||||
totalBlocks += mdlBlock.VertexBufferBlockNum[ i ];
|
||||
}
|
||||
|
||||
for( var i = 0; i < 3; i++ )
|
||||
{
|
||||
totalBlocks += mdlBlock.EdgeGeometryVertexBufferBlockNum[ i ];
|
||||
}
|
||||
|
||||
for( var i = 0; i < 3; i++ )
|
||||
{
|
||||
totalBlocks += mdlBlock.IndexBufferBlockNum[ i ];
|
||||
}
|
||||
|
||||
var compressedBlockSizes = Reader.ReadStructures< ushort >( totalBlocks );
|
||||
var currentBlock = 0;
|
||||
var vertexDataOffsets = new int[3];
|
||||
var indexDataOffsets = new int[3];
|
||||
var vertexBufferSizes = new int[3];
|
||||
var indexBufferSizes = new int[3];
|
||||
|
||||
ms.Seek( 0x44, SeekOrigin.Begin );
|
||||
|
||||
Reader.Seek( baseOffset + mdlBlock.StackOffset );
|
||||
var stackStart = ms.Position;
|
||||
for( var i = 0; i < mdlBlock.StackBlockNum; i++ )
|
||||
{
|
||||
var lastPos = Reader.BaseStream.Position;
|
||||
ReadFileBlock( ms );
|
||||
Reader.Seek( lastPos + compressedBlockSizes[ currentBlock ] );
|
||||
currentBlock++;
|
||||
}
|
||||
|
||||
var stackEnd = ms.Position;
|
||||
var stackSize = ( int )( stackEnd - stackStart );
|
||||
|
||||
Reader.Seek( baseOffset + mdlBlock.RuntimeOffset );
|
||||
var runtimeStart = ms.Position;
|
||||
for( var i = 0; i < mdlBlock.RuntimeBlockNum; i++ )
|
||||
{
|
||||
var lastPos = Reader.BaseStream.Position;
|
||||
ReadFileBlock( ms );
|
||||
Reader.Seek( lastPos + compressedBlockSizes[ currentBlock ] );
|
||||
currentBlock++;
|
||||
}
|
||||
|
||||
var runtimeEnd = ms.Position;
|
||||
var runtimeSize = ( int )( runtimeEnd - runtimeStart );
|
||||
|
||||
for( var i = 0; i < 3; i++ )
|
||||
{
|
||||
if( mdlBlock.VertexBufferBlockNum[ i ] != 0 )
|
||||
var currentVertexOffset = ( int )ms.Position;
|
||||
if( i == 0 || currentVertexOffset != vertexDataOffsets[ i - 1 ] )
|
||||
{
|
||||
var currentVertexOffset = ( int )ms.Position;
|
||||
if( i == 0 || currentVertexOffset != vertexDataOffsets[ i - 1 ] )
|
||||
{
|
||||
vertexDataOffsets[ i ] = currentVertexOffset;
|
||||
}
|
||||
else
|
||||
{
|
||||
vertexDataOffsets[ i ] = 0;
|
||||
}
|
||||
|
||||
Reader.Seek( baseOffset + mdlBlock.VertexBufferOffset[ i ] );
|
||||
|
||||
for( var j = 0; j < mdlBlock.VertexBufferBlockNum[ i ]; j++ )
|
||||
{
|
||||
var lastPos = Reader.BaseStream.Position;
|
||||
vertexBufferSizes[ i ] += ( int )ReadFileBlock( ms );
|
||||
Reader.Seek( lastPos + compressedBlockSizes[ currentBlock ] );
|
||||
currentBlock++;
|
||||
}
|
||||
vertexDataOffsets[ i ] = currentVertexOffset;
|
||||
}
|
||||
else
|
||||
{
|
||||
vertexDataOffsets[ i ] = 0;
|
||||
}
|
||||
|
||||
if( mdlBlock.EdgeGeometryVertexBufferBlockNum[ i ] != 0 )
|
||||
Reader.Seek( baseOffset + mdlBlock.VertexBufferOffset[ i ] );
|
||||
|
||||
for( var j = 0; j < mdlBlock.VertexBufferBlockNum[ i ]; j++ )
|
||||
{
|
||||
for( var j = 0; j < mdlBlock.EdgeGeometryVertexBufferBlockNum[ i ]; j++ )
|
||||
{
|
||||
var lastPos = Reader.BaseStream.Position;
|
||||
ReadFileBlock( ms );
|
||||
Reader.Seek( lastPos + compressedBlockSizes[ currentBlock ] );
|
||||
currentBlock++;
|
||||
}
|
||||
}
|
||||
|
||||
if( mdlBlock.IndexBufferBlockNum[ i ] != 0 )
|
||||
{
|
||||
var currentIndexOffset = ( int )ms.Position;
|
||||
if( i == 0 || currentIndexOffset != indexDataOffsets[ i - 1 ] )
|
||||
{
|
||||
indexDataOffsets[ i ] = currentIndexOffset;
|
||||
}
|
||||
else
|
||||
{
|
||||
indexDataOffsets[ i ] = 0;
|
||||
}
|
||||
|
||||
// i guess this is only needed in the vertex area, for i = 0
|
||||
// Reader.Seek( baseOffset + mdlBlock.IndexBufferOffset[ i ] );
|
||||
|
||||
for( var j = 0; j < mdlBlock.IndexBufferBlockNum[ i ]; j++ )
|
||||
{
|
||||
var lastPos = Reader.BaseStream.Position;
|
||||
indexBufferSizes[ i ] += ( int )ReadFileBlock( ms );
|
||||
Reader.Seek( lastPos + compressedBlockSizes[ currentBlock ] );
|
||||
currentBlock++;
|
||||
}
|
||||
var lastPos = Reader.BaseStream.Position;
|
||||
vertexBufferSizes[ i ] += ( int )ReadFileBlock( ms );
|
||||
Reader.Seek( lastPos + compressedBlockSizes[ currentBlock ] );
|
||||
currentBlock++;
|
||||
}
|
||||
}
|
||||
|
||||
ms.Seek( 0, SeekOrigin.Begin );
|
||||
ms.Write( BitConverter.GetBytes( mdlBlock.Version ) );
|
||||
ms.Write( BitConverter.GetBytes( stackSize ) );
|
||||
ms.Write( BitConverter.GetBytes( runtimeSize ) );
|
||||
ms.Write( BitConverter.GetBytes( mdlBlock.VertexDeclarationNum ) );
|
||||
ms.Write( BitConverter.GetBytes( mdlBlock.MaterialNum ) );
|
||||
for( var i = 0; i < 3; i++ )
|
||||
if( mdlBlock.EdgeGeometryVertexBufferBlockNum[ i ] != 0 )
|
||||
{
|
||||
ms.Write( BitConverter.GetBytes( vertexDataOffsets[ i ] ) );
|
||||
for( var j = 0; j < mdlBlock.EdgeGeometryVertexBufferBlockNum[ i ]; j++ )
|
||||
{
|
||||
var lastPos = Reader.BaseStream.Position;
|
||||
ReadFileBlock( ms );
|
||||
Reader.Seek( lastPos + compressedBlockSizes[ currentBlock ] );
|
||||
currentBlock++;
|
||||
}
|
||||
}
|
||||
|
||||
for( var i = 0; i < 3; i++ )
|
||||
if( mdlBlock.IndexBufferBlockNum[ i ] != 0 )
|
||||
{
|
||||
ms.Write( BitConverter.GetBytes( indexDataOffsets[ i ] ) );
|
||||
}
|
||||
var currentIndexOffset = ( int )ms.Position;
|
||||
if( i == 0 || currentIndexOffset != indexDataOffsets[ i - 1 ] )
|
||||
{
|
||||
indexDataOffsets[ i ] = currentIndexOffset;
|
||||
}
|
||||
else
|
||||
{
|
||||
indexDataOffsets[ i ] = 0;
|
||||
}
|
||||
|
||||
for( var i = 0; i < 3; i++ )
|
||||
{
|
||||
ms.Write( BitConverter.GetBytes( vertexBufferSizes[ i ] ) );
|
||||
}
|
||||
// i guess this is only needed in the vertex area, for i = 0
|
||||
// Reader.Seek( baseOffset + mdlBlock.IndexBufferOffset[ i ] );
|
||||
|
||||
for( var i = 0; i < 3; i++ )
|
||||
{
|
||||
ms.Write( BitConverter.GetBytes( indexBufferSizes[ i ] ) );
|
||||
for( var j = 0; j < mdlBlock.IndexBufferBlockNum[ i ]; j++ )
|
||||
{
|
||||
var lastPos = Reader.BaseStream.Position;
|
||||
indexBufferSizes[ i ] += ( int )ReadFileBlock( ms );
|
||||
Reader.Seek( lastPos + compressedBlockSizes[ currentBlock ] );
|
||||
currentBlock++;
|
||||
}
|
||||
}
|
||||
|
||||
ms.Write( new[] { mdlBlock.NumLods } );
|
||||
ms.Write( BitConverter.GetBytes( mdlBlock.IndexBufferStreamingEnabled ) );
|
||||
ms.Write( BitConverter.GetBytes( mdlBlock.EdgeGeometryEnabled ) );
|
||||
ms.Write( new byte[] { 0 } );
|
||||
}
|
||||
|
||||
private void ReadTextureFile( PenumbraFileResource resource, MemoryStream ms )
|
||||
ms.Seek( 0, SeekOrigin.Begin );
|
||||
ms.Write( BitConverter.GetBytes( mdlBlock.Version ) );
|
||||
ms.Write( BitConverter.GetBytes( stackSize ) );
|
||||
ms.Write( BitConverter.GetBytes( runtimeSize ) );
|
||||
ms.Write( BitConverter.GetBytes( mdlBlock.VertexDeclarationNum ) );
|
||||
ms.Write( BitConverter.GetBytes( mdlBlock.MaterialNum ) );
|
||||
for( var i = 0; i < 3; i++ )
|
||||
{
|
||||
var blocks = Reader.ReadStructures< LodBlock >( ( int )resource.FileInfo!.BlockCount );
|
||||
ms.Write( BitConverter.GetBytes( vertexDataOffsets[ i ] ) );
|
||||
}
|
||||
|
||||
// if there is a mipmap header, the comp_offset
|
||||
// will not be 0
|
||||
var mipMapSize = blocks[ 0 ].CompressedOffset;
|
||||
if( mipMapSize != 0 )
|
||||
for( var i = 0; i < 3; i++ )
|
||||
{
|
||||
ms.Write( BitConverter.GetBytes( indexDataOffsets[ i ] ) );
|
||||
}
|
||||
|
||||
for( var i = 0; i < 3; i++ )
|
||||
{
|
||||
ms.Write( BitConverter.GetBytes( vertexBufferSizes[ i ] ) );
|
||||
}
|
||||
|
||||
for( var i = 0; i < 3; i++ )
|
||||
{
|
||||
ms.Write( BitConverter.GetBytes( indexBufferSizes[ i ] ) );
|
||||
}
|
||||
|
||||
ms.Write( new[] { mdlBlock.NumLods } );
|
||||
ms.Write( BitConverter.GetBytes( mdlBlock.IndexBufferStreamingEnabled ) );
|
||||
ms.Write( BitConverter.GetBytes( mdlBlock.EdgeGeometryEnabled ) );
|
||||
ms.Write( new byte[] { 0 } );
|
||||
}
|
||||
|
||||
private void ReadTextureFile( PenumbraFileResource resource, MemoryStream ms )
|
||||
{
|
||||
var blocks = Reader.ReadStructures< LodBlock >( ( int )resource.FileInfo!.BlockCount );
|
||||
|
||||
// if there is a mipmap header, the comp_offset
|
||||
// will not be 0
|
||||
var mipMapSize = blocks[ 0 ].CompressedOffset;
|
||||
if( mipMapSize != 0 )
|
||||
{
|
||||
var originalPos = BaseStream.Position;
|
||||
|
||||
BaseStream.Position = resource.FileInfo.Offset + resource.FileInfo.HeaderSize;
|
||||
ms.Write( Reader.ReadBytes( ( int )mipMapSize ) );
|
||||
|
||||
BaseStream.Position = originalPos;
|
||||
}
|
||||
|
||||
// i is for texture blocks, j is 'data blocks'...
|
||||
for( byte i = 0; i < blocks.Count; i++ )
|
||||
{
|
||||
// start from comp_offset
|
||||
var runningBlockTotal = blocks[ i ].CompressedOffset + resource.FileInfo.Offset + resource.FileInfo.HeaderSize;
|
||||
ReadFileBlock( runningBlockTotal, ms, true );
|
||||
|
||||
for( var j = 1; j < blocks[ i ].BlockCount; j++ )
|
||||
{
|
||||
var originalPos = BaseStream.Position;
|
||||
|
||||
BaseStream.Position = resource.FileInfo.Offset + resource.FileInfo.HeaderSize;
|
||||
ms.Write( Reader.ReadBytes( ( int )mipMapSize ) );
|
||||
|
||||
BaseStream.Position = originalPos;
|
||||
}
|
||||
|
||||
// i is for texture blocks, j is 'data blocks'...
|
||||
for( byte i = 0; i < blocks.Count; i++ )
|
||||
{
|
||||
// start from comp_offset
|
||||
var runningBlockTotal = blocks[ i ].CompressedOffset + resource.FileInfo.Offset + resource.FileInfo.HeaderSize;
|
||||
runningBlockTotal += ( uint )Reader.ReadInt16();
|
||||
ReadFileBlock( runningBlockTotal, ms, true );
|
||||
|
||||
for( var j = 1; j < blocks[ i ].BlockCount; j++ )
|
||||
{
|
||||
runningBlockTotal += ( uint )Reader.ReadInt16();
|
||||
ReadFileBlock( runningBlockTotal, ms, true );
|
||||
}
|
||||
|
||||
// unknown
|
||||
Reader.ReadInt16();
|
||||
}
|
||||
|
||||
// unknown
|
||||
Reader.ReadInt16();
|
||||
}
|
||||
}
|
||||
|
||||
protected uint ReadFileBlock( MemoryStream dest, bool resetPosition = false )
|
||||
=> ReadFileBlock( Reader.BaseStream.Position, dest, resetPosition );
|
||||
protected uint ReadFileBlock( MemoryStream dest, bool resetPosition = false )
|
||||
=> ReadFileBlock( Reader.BaseStream.Position, dest, resetPosition );
|
||||
|
||||
protected uint ReadFileBlock( long offset, MemoryStream dest, bool resetPosition = false )
|
||||
protected uint ReadFileBlock( long offset, MemoryStream dest, bool resetPosition = false )
|
||||
{
|
||||
var originalPosition = BaseStream.Position;
|
||||
BaseStream.Position = offset;
|
||||
|
||||
var blockHeader = Reader.ReadStructure< DatBlockHeader >();
|
||||
|
||||
// uncompressed block
|
||||
if( blockHeader.CompressedSize == 32000 )
|
||||
{
|
||||
var originalPosition = BaseStream.Position;
|
||||
BaseStream.Position = offset;
|
||||
|
||||
var blockHeader = Reader.ReadStructure< DatBlockHeader >();
|
||||
|
||||
// uncompressed block
|
||||
if( blockHeader.CompressedSize == 32000 )
|
||||
{
|
||||
dest.Write( Reader.ReadBytes( ( int )blockHeader.UncompressedSize ) );
|
||||
return blockHeader.UncompressedSize;
|
||||
}
|
||||
|
||||
var data = Reader.ReadBytes( ( int )blockHeader.CompressedSize );
|
||||
|
||||
using( var compressedStream = new MemoryStream( data ) )
|
||||
{
|
||||
using var zlibStream = new DeflateStream( compressedStream, CompressionMode.Decompress );
|
||||
zlibStream.CopyTo( dest );
|
||||
}
|
||||
|
||||
if( resetPosition )
|
||||
{
|
||||
BaseStream.Position = originalPosition;
|
||||
}
|
||||
|
||||
dest.Write( Reader.ReadBytes( ( int )blockHeader.UncompressedSize ) );
|
||||
return blockHeader.UncompressedSize;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
var data = Reader.ReadBytes( ( int )blockHeader.CompressedSize );
|
||||
|
||||
using( var compressedStream = new MemoryStream( data ) )
|
||||
{
|
||||
Reader?.Dispose();
|
||||
using var zlibStream = new DeflateStream( compressedStream, CompressionMode.Decompress );
|
||||
zlibStream.CopyTo( dest );
|
||||
}
|
||||
|
||||
public class PenumbraFileInfo
|
||||
if( resetPosition )
|
||||
{
|
||||
public uint HeaderSize;
|
||||
public FileType Type;
|
||||
public uint RawFileSize;
|
||||
public uint BlockCount;
|
||||
|
||||
public long Offset { get; internal set; }
|
||||
|
||||
public ModelBlock ModelBlock { get; internal set; }
|
||||
BaseStream.Position = originalPosition;
|
||||
}
|
||||
|
||||
public class PenumbraFileResource
|
||||
return blockHeader.UncompressedSize;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Reader.Dispose();
|
||||
}
|
||||
|
||||
public class PenumbraFileInfo
|
||||
{
|
||||
public uint HeaderSize;
|
||||
public FileType Type;
|
||||
public uint RawFileSize;
|
||||
public uint BlockCount;
|
||||
|
||||
public long Offset { get; internal set; }
|
||||
|
||||
public ModelBlock ModelBlock { get; internal set; }
|
||||
}
|
||||
|
||||
public class PenumbraFileResource
|
||||
{
|
||||
public PenumbraFileResource()
|
||||
{ }
|
||||
|
||||
public PenumbraFileInfo? FileInfo { get; internal set; }
|
||||
|
||||
public byte[] Data { get; internal set; } = new byte[0];
|
||||
|
||||
public MemoryStream? FileStream { get; internal set; }
|
||||
|
||||
public BinaryReader? Reader { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Called once the files are read out from the dats. Used to further parse the file into usable data structures.
|
||||
/// </summary>
|
||||
public virtual void LoadFile()
|
||||
{
|
||||
public PenumbraFileResource()
|
||||
{ }
|
||||
|
||||
public PenumbraFileInfo? FileInfo { get; internal set; }
|
||||
|
||||
public byte[] Data { get; internal set; } = new byte[0];
|
||||
|
||||
public Span< byte > DataSpan
|
||||
=> Data.AsSpan();
|
||||
|
||||
public MemoryStream? FileStream { get; internal set; }
|
||||
|
||||
public BinaryReader? Reader { get; internal set; }
|
||||
|
||||
public ParsedFilePath? FilePath { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Called once the files are read out from the dats. Used to further parse the file into usable data structures.
|
||||
/// </summary>
|
||||
public virtual void LoadFile()
|
||||
{
|
||||
// this function is intentionally left blank
|
||||
}
|
||||
|
||||
public virtual void SaveFile( string path )
|
||||
{
|
||||
File.WriteAllBytes( path, Data );
|
||||
}
|
||||
|
||||
public string GetFileHash()
|
||||
{
|
||||
using var sha256 = System.Security.Cryptography.SHA256.Create();
|
||||
var hash = sha256.ComputeHash( Data );
|
||||
|
||||
var sb = new StringBuilder();
|
||||
foreach( var b in hash )
|
||||
{
|
||||
sb.Append( $"{b:x2}" );
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout( LayoutKind.Sequential )]
|
||||
private struct DatBlockHeader
|
||||
{
|
||||
public uint Size;
|
||||
public uint unknown1;
|
||||
public uint CompressedSize;
|
||||
public uint UncompressedSize;
|
||||
};
|
||||
|
||||
[StructLayout( LayoutKind.Sequential )]
|
||||
private struct LodBlock
|
||||
{
|
||||
public uint CompressedOffset;
|
||||
public uint CompressedSize;
|
||||
public uint DecompressedSize;
|
||||
public uint BlockOffset;
|
||||
public uint BlockCount;
|
||||
// this function is intentionally left blank
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout( LayoutKind.Sequential )]
|
||||
private struct DatBlockHeader
|
||||
{
|
||||
public uint Size;
|
||||
public uint unknown1;
|
||||
public uint CompressedSize;
|
||||
public uint UncompressedSize;
|
||||
};
|
||||
|
||||
[StructLayout( LayoutKind.Sequential )]
|
||||
private struct LodBlock
|
||||
{
|
||||
public uint CompressedOffset;
|
||||
public uint CompressedSize;
|
||||
public uint DecompressedSize;
|
||||
public uint BlockOffset;
|
||||
public uint BlockCount;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,44 +3,43 @@ using System.Collections.Generic;
|
|||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Penumbra.Util
|
||||
namespace Penumbra.Util;
|
||||
|
||||
public class SingleOrArrayConverter< T > : JsonConverter
|
||||
{
|
||||
public class SingleOrArrayConverter< T > : JsonConverter
|
||||
public override bool CanConvert( Type objectType )
|
||||
=> objectType == typeof( HashSet< T > );
|
||||
|
||||
public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer )
|
||||
{
|
||||
public override bool CanConvert( Type objectType )
|
||||
=> objectType == typeof( HashSet< T > );
|
||||
var token = JToken.Load( reader );
|
||||
|
||||
public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer )
|
||||
if( token.Type == JTokenType.Array )
|
||||
{
|
||||
var token = JToken.Load( reader );
|
||||
|
||||
if( token.Type == JTokenType.Array )
|
||||
{
|
||||
return token.ToObject< HashSet< T > >() ?? new HashSet< T >();
|
||||
}
|
||||
|
||||
var tmp = token.ToObject< T >();
|
||||
return tmp != null
|
||||
? new HashSet< T > { tmp }
|
||||
: new HashSet< T >();
|
||||
return token.ToObject< HashSet< T > >() ?? new HashSet< T >();
|
||||
}
|
||||
|
||||
public override bool CanWrite
|
||||
=> true;
|
||||
var tmp = token.ToObject< T >();
|
||||
return tmp != null
|
||||
? new HashSet< T > { tmp }
|
||||
: new HashSet< T >();
|
||||
}
|
||||
|
||||
public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer )
|
||||
public override bool CanWrite
|
||||
=> true;
|
||||
|
||||
public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer )
|
||||
{
|
||||
writer.WriteStartArray();
|
||||
if( value != null )
|
||||
{
|
||||
writer.WriteStartArray();
|
||||
if( value != null )
|
||||
var v = ( HashSet< T > )value;
|
||||
foreach( var val in v )
|
||||
{
|
||||
var v = ( HashSet< T > )value;
|
||||
foreach( var val in v )
|
||||
{
|
||||
serializer.Serialize( writer, val?.ToString() );
|
||||
}
|
||||
serializer.Serialize( writer, val?.ToString() );
|
||||
}
|
||||
|
||||
writer.WriteEndArray();
|
||||
}
|
||||
|
||||
writer.WriteEndArray();
|
||||
}
|
||||
}
|
||||
|
|
@ -2,67 +2,66 @@ using System.Collections.Generic;
|
|||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace Penumbra.Util
|
||||
namespace Penumbra.Util;
|
||||
|
||||
public static class StringPathExtensions
|
||||
{
|
||||
public static class StringPathExtensions
|
||||
private static readonly HashSet< char > Invalid = new(Path.GetInvalidFileNameChars());
|
||||
|
||||
public static string ReplaceInvalidPathSymbols( this string s, string replacement = "_" )
|
||||
{
|
||||
private static readonly HashSet< char > Invalid = new( Path.GetInvalidFileNameChars() );
|
||||
|
||||
public static string ReplaceInvalidPathSymbols( this string s, string replacement = "_" )
|
||||
StringBuilder sb = new(s.Length);
|
||||
foreach( var c in s )
|
||||
{
|
||||
StringBuilder sb = new( s.Length );
|
||||
foreach( var c in s )
|
||||
if( Invalid.Contains( c ) )
|
||||
{
|
||||
if( Invalid.Contains( c ) )
|
||||
{
|
||||
sb.Append( replacement );
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append( c );
|
||||
}
|
||||
sb.Append( replacement );
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append( c );
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public static string RemoveInvalidPathSymbols( this string s )
|
||||
=> string.Concat( s.Split( Path.GetInvalidFileNameChars() ) );
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public static string ReplaceNonAsciiSymbols( this string s, string replacement = "_" )
|
||||
public static string RemoveInvalidPathSymbols( this string s )
|
||||
=> string.Concat( s.Split( Path.GetInvalidFileNameChars() ) );
|
||||
|
||||
public static string ReplaceNonAsciiSymbols( this string s, string replacement = "_" )
|
||||
{
|
||||
StringBuilder sb = new(s.Length);
|
||||
foreach( var c in s )
|
||||
{
|
||||
StringBuilder sb = new( s.Length );
|
||||
foreach( var c in s )
|
||||
if( c >= 128 )
|
||||
{
|
||||
if( c >= 128 )
|
||||
{
|
||||
sb.Append( replacement );
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append( c );
|
||||
}
|
||||
sb.Append( replacement );
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append( c );
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public static string ReplaceBadXivSymbols( this string s, string replacement = "_" )
|
||||
{
|
||||
StringBuilder sb = new( s.Length );
|
||||
foreach( var c in s )
|
||||
{
|
||||
if( c >= 128 || Invalid.Contains( c ) )
|
||||
{
|
||||
sb.Append( replacement );
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append( c );
|
||||
}
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
public static string ReplaceBadXivSymbols( this string s, string replacement = "_" )
|
||||
{
|
||||
StringBuilder sb = new(s.Length);
|
||||
foreach( var c in s )
|
||||
{
|
||||
if( c >= 128 || Invalid.Contains( c ) )
|
||||
{
|
||||
sb.Append( replacement );
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append( c );
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,34 +1,33 @@
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace Penumbra.Util
|
||||
namespace Penumbra.Util;
|
||||
|
||||
public static class TempFile
|
||||
{
|
||||
public static class TempFile
|
||||
public static FileInfo TempFileName( DirectoryInfo baseDir, string suffix = "" )
|
||||
{
|
||||
public static FileInfo TempFileName( DirectoryInfo baseDir, string suffix = "" )
|
||||
const uint maxTries = 15;
|
||||
for( var i = 0; i < maxTries; ++i )
|
||||
{
|
||||
const uint maxTries = 15;
|
||||
for( var i = 0; i < maxTries; ++i )
|
||||
var name = Path.GetRandomFileName();
|
||||
var path = new FileInfo( Path.Combine( baseDir.FullName,
|
||||
suffix.Any() ? name.Substring( 0, name.LastIndexOf( '.' ) ) + suffix : name ) );
|
||||
if( !path.Exists )
|
||||
{
|
||||
var name = Path.GetRandomFileName();
|
||||
var path = new FileInfo( Path.Combine( baseDir.FullName,
|
||||
suffix.Any() ? name.Substring( 0, name.LastIndexOf( '.' ) ) + suffix : name ) );
|
||||
if( !path.Exists )
|
||||
{
|
||||
return path;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
throw new IOException();
|
||||
}
|
||||
|
||||
public static FileInfo WriteNew( DirectoryInfo baseDir, byte[] data, string suffix = "" )
|
||||
{
|
||||
var fileName = TempFileName( baseDir, suffix );
|
||||
using var stream = fileName.OpenWrite();
|
||||
stream.Write( data, 0, data.Length );
|
||||
fileName.Refresh();
|
||||
return fileName;
|
||||
}
|
||||
throw new IOException();
|
||||
}
|
||||
|
||||
public static FileInfo WriteNew( DirectoryInfo baseDir, byte[] data, string suffix = "" )
|
||||
{
|
||||
var fileName = TempFileName( baseDir, suffix );
|
||||
using var stream = fileName.OpenWrite();
|
||||
stream.Write( data, 0, data.Length );
|
||||
fileName.Refresh();
|
||||
return fileName;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue