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;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
using Penumbra.GameData.Util;
|
using Penumbra.GameData.Util;
|
||||||
|
|
||||||
namespace Penumbra.GameData.ByteString;
|
namespace Penumbra.GameData.ByteString;
|
||||||
|
|
||||||
|
[JsonConverter( typeof( FullPathConverter ) )]
|
||||||
public readonly struct FullPath : IComparable, IEquatable< FullPath >
|
public readonly struct FullPath : IComparable, IEquatable< FullPath >
|
||||||
{
|
{
|
||||||
public readonly string FullName;
|
public readonly string FullName;
|
||||||
public readonly Utf8String InternalName;
|
public readonly Utf8String InternalName;
|
||||||
public readonly ulong Crc64;
|
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() ) )
|
: this( Path.Combine( baseDir.FullName, relPath.ToString() ) )
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
|
|
@ -19,10 +23,11 @@ public readonly struct FullPath : IComparable, IEquatable< FullPath >
|
||||||
: this( file.FullName )
|
: this( file.FullName )
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
|
|
||||||
public FullPath( string s )
|
public FullPath( string s )
|
||||||
{
|
{
|
||||||
FullName = 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 );
|
Crc64 = Functions.ComputeCrc64( InternalName.Span );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -35,9 +40,9 @@ public readonly struct FullPath : IComparable, IEquatable< FullPath >
|
||||||
public string Name
|
public string Name
|
||||||
=> Path.GetFileName( FullName );
|
=> 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 ) )
|
if( !InternalName.IsAscii || !FullName.StartsWith( dir.FullName ) )
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -45,13 +50,13 @@ public readonly struct FullPath : IComparable, IEquatable< FullPath >
|
||||||
|
|
||||||
var substring = InternalName.Substring( dir.FullName.Length + 1 );
|
var substring = InternalName.Substring( dir.FullName.Length + 1 );
|
||||||
|
|
||||||
path = new NewGamePath( substring.Replace( ( byte )'\\', ( byte )'/' ) );
|
path = new Utf8GamePath( substring );
|
||||||
return true;
|
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 ) )
|
if( !FullName.StartsWith( dir.FullName ) )
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -59,7 +64,7 @@ public readonly struct FullPath : IComparable, IEquatable< FullPath >
|
||||||
|
|
||||||
var substring = InternalName.Substring( dir.FullName.Length + 1 );
|
var substring = InternalName.Substring( dir.FullName.Length + 1 );
|
||||||
|
|
||||||
path = new NewRelPath( substring );
|
path = new Utf8RelPath( substring.Replace( ( byte )'/', ( byte )'\\' ) );
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -88,9 +93,35 @@ public readonly struct FullPath : IComparable, IEquatable< FullPath >
|
||||||
return InternalName.Equals( other.InternalName );
|
return InternalName.Equals( other.InternalName );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool IsRooted
|
||||||
|
=> new Utf8GamePath( InternalName ).IsRooted();
|
||||||
|
|
||||||
public override int GetHashCode()
|
public override int GetHashCode()
|
||||||
=> InternalName.Crc32;
|
=> InternalName.Crc32;
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
=> FullName;
|
=> 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 Dalamud.Utility;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
using Penumbra.GameData.Util;
|
||||||
|
|
||||||
namespace Penumbra.GameData.ByteString;
|
namespace Penumbra.GameData.ByteString;
|
||||||
|
|
||||||
// NewGamePath wrap some additional validity checking around Utf8String,
|
// NewGamePath wrap some additional validity checking around Utf8String,
|
||||||
// provide some filesystem helpers, and conversion to Json.
|
// provide some filesystem helpers, and conversion to Json.
|
||||||
[JsonConverter( typeof( NewGamePathConverter ) )]
|
[JsonConverter( typeof( Utf8GamePathConverter ) )]
|
||||||
public readonly struct NewGamePath : IEquatable< NewGamePath >, IComparable< NewGamePath >, IDisposable
|
public readonly struct Utf8GamePath : IEquatable< Utf8GamePath >, IComparable< Utf8GamePath >, IDisposable
|
||||||
{
|
{
|
||||||
public const int MaxGamePathLength = 256;
|
public const int MaxGamePathLength = 256;
|
||||||
|
|
||||||
public readonly Utf8String Path;
|
public readonly Utf8String Path;
|
||||||
public static readonly NewGamePath Empty = new(Utf8String.Empty);
|
public static readonly Utf8GamePath Empty = new(Utf8String.Empty);
|
||||||
|
|
||||||
internal NewGamePath( Utf8String s )
|
internal Utf8GamePath( Utf8String s )
|
||||||
=> Path = s;
|
=> Path = s;
|
||||||
|
|
||||||
public int Length
|
public int Length
|
||||||
|
|
@ -25,16 +26,16 @@ public readonly struct NewGamePath : IEquatable< NewGamePath >, IComparable< New
|
||||||
public bool IsEmpty
|
public bool IsEmpty
|
||||||
=> Path.IsEmpty;
|
=> Path.IsEmpty;
|
||||||
|
|
||||||
public NewGamePath ToLower()
|
public Utf8GamePath ToLower()
|
||||||
=> new(Path.AsciiToLower());
|
=> 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 );
|
var utf = new Utf8String( ptr );
|
||||||
return ReturnChecked( utf, out path, lower );
|
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 );
|
var utf = Utf8String.FromSpanUnsafe( data, false, null, null );
|
||||||
return ReturnChecked( utf, out path, lower );
|
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 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.
|
// Does not check for initial slashes either, since they are assumed to be by choice.
|
||||||
// Checks for maxlength, ASCII and lowercase.
|
// 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;
|
path = Empty;
|
||||||
if( !utf.IsAscii || utf.Length > MaxGamePathLength )
|
if( !utf.IsAscii || utf.Length > MaxGamePathLength )
|
||||||
|
|
@ -51,14 +52,17 @@ public readonly struct NewGamePath : IEquatable< NewGamePath >, IComparable< New
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
path = new NewGamePath( lower ? utf.AsciiToLower() : utf );
|
path = new Utf8GamePath( lower ? utf.AsciiToLower() : utf );
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public NewGamePath Clone()
|
public Utf8GamePath Clone()
|
||||||
=> new(Path.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;
|
path = Empty;
|
||||||
if( s.IsNullOrEmpty() )
|
if( s.IsNullOrEmpty() )
|
||||||
|
|
@ -83,11 +87,11 @@ public readonly struct NewGamePath : IEquatable< NewGamePath >, IComparable< New
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
path = new NewGamePath( ascii );
|
path = new Utf8GamePath( ascii );
|
||||||
return true;
|
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;
|
path = Empty;
|
||||||
if( !file.FullName.StartsWith( baseDir.FullName ) )
|
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 );
|
return idx == -1 ? Utf8String.Empty : Path.Substring( idx );
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Equals( NewGamePath other )
|
public bool Equals( Utf8GamePath other )
|
||||||
=> Path.Equals( other.Path );
|
=> Path.Equals( other.Path );
|
||||||
|
|
||||||
public override int GetHashCode()
|
public override int GetHashCode()
|
||||||
=> Path.GetHashCode();
|
=> Path.GetHashCode();
|
||||||
|
|
||||||
public int CompareTo( NewGamePath other )
|
public int CompareTo( Utf8GamePath other )
|
||||||
=> Path.CompareTo( other.Path );
|
=> Path.CompareTo( other.Path );
|
||||||
|
|
||||||
public override string ToString()
|
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[ 0 ] >= 'A' && Path[ 0 ] <= 'Z' || Path[ 0 ] >= 'a' && Path[ 0 ] <= 'z' )
|
||||||
&& Path[ 1 ] == ':';
|
&& Path[ 1 ] == ':';
|
||||||
|
|
||||||
private class NewGamePathConverter : JsonConverter
|
public class Utf8GamePathConverter : JsonConverter
|
||||||
{
|
{
|
||||||
public override bool CanConvert( Type objectType )
|
public override bool CanConvert( Type objectType )
|
||||||
=> objectType == typeof( NewGamePath );
|
=> objectType == typeof( Utf8GamePath );
|
||||||
|
|
||||||
public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer )
|
public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer )
|
||||||
{
|
{
|
||||||
var token = JToken.Load( reader ).ToString();
|
var token = JToken.Load( reader ).ToString();
|
||||||
return FromString( token, out var p, true )
|
return FromString( token, out var p, true )
|
||||||
? p
|
? 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
|
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 )
|
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() );
|
serializer.Serialize( writer, p.ToString() );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public GamePath ToGamePath()
|
||||||
|
=> GamePath.GenerateUnchecked( ToString() );
|
||||||
}
|
}
|
||||||
|
|
@ -1,23 +1,28 @@
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using Dalamud.Utility;
|
using Dalamud.Utility;
|
||||||
|
using Microsoft.VisualBasic.CompilerServices;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace Penumbra.GameData.ByteString;
|
namespace Penumbra.GameData.ByteString;
|
||||||
|
|
||||||
[JsonConverter( typeof( NewRelPathConverter ) )]
|
[JsonConverter( typeof( Utf8RelPathConverter ) )]
|
||||||
public readonly struct NewRelPath : IEquatable< NewRelPath >, IComparable< NewRelPath >, IDisposable
|
public readonly struct Utf8RelPath : IEquatable< Utf8RelPath >, IComparable< Utf8RelPath >, IDisposable
|
||||||
{
|
{
|
||||||
public const int MaxRelPathLength = 250;
|
public const int MaxRelPathLength = 250;
|
||||||
|
|
||||||
public readonly Utf8String Path;
|
public readonly Utf8String Path;
|
||||||
public static readonly NewRelPath Empty = new(Utf8String.Empty);
|
public static readonly Utf8RelPath Empty = new(Utf8String.Empty);
|
||||||
|
|
||||||
internal NewRelPath( Utf8String path )
|
internal Utf8RelPath( Utf8String path )
|
||||||
=> Path = 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;
|
path = Empty;
|
||||||
if( s.IsNullOrEmpty() )
|
if( s.IsNullOrEmpty() )
|
||||||
|
|
@ -42,11 +47,11 @@ public readonly struct NewRelPath : IEquatable< NewRelPath >, IComparable< NewRe
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
path = new NewRelPath( ascii );
|
path = new Utf8RelPath( ascii );
|
||||||
return true;
|
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;
|
path = Empty;
|
||||||
if( !file.FullName.StartsWith( baseDir.FullName ) )
|
if( !file.FullName.StartsWith( baseDir.FullName ) )
|
||||||
|
|
@ -58,7 +63,7 @@ public readonly struct NewRelPath : IEquatable< NewRelPath >, IComparable< NewRe
|
||||||
return FromString( substring, out path );
|
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;
|
path = Empty;
|
||||||
if( !file.FullName.StartsWith( baseDir.FullName ) )
|
if( !file.FullName.StartsWith( baseDir.FullName ) )
|
||||||
|
|
@ -70,10 +75,10 @@ public readonly struct NewRelPath : IEquatable< NewRelPath >, IComparable< NewRe
|
||||||
return FromString( substring, out path );
|
return FromString( substring, out path );
|
||||||
}
|
}
|
||||||
|
|
||||||
public NewRelPath( NewGamePath gamePath )
|
public Utf8RelPath( Utf8GamePath gamePath )
|
||||||
=> Path = gamePath.Path.Replace( ( byte )'/', ( byte )'\\' );
|
=> Path = gamePath.Path.Replace( ( byte )'/', ( byte )'\\' );
|
||||||
|
|
||||||
public unsafe NewGamePath ToGamePath( int skipFolders = 0 )
|
public unsafe Utf8GamePath ToGamePath( int skipFolders = 0 )
|
||||||
{
|
{
|
||||||
var idx = 0;
|
var idx = 0;
|
||||||
while( skipFolders > 0 )
|
while( skipFolders > 0 )
|
||||||
|
|
@ -82,7 +87,7 @@ public readonly struct NewRelPath : IEquatable< NewRelPath >, IComparable< NewRe
|
||||||
--skipFolders;
|
--skipFolders;
|
||||||
if( idx <= 0 )
|
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.Replace( ptr, length, ( byte )'\\', ( byte )'/' );
|
||||||
ByteStringFunctions.AsciiToLowerInPlace( ptr, length );
|
ByteStringFunctions.AsciiToLowerInPlace( ptr, length );
|
||||||
var utf = new Utf8String().Setup( ptr, length, null, true, true, true, true );
|
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 );
|
=> Path.CompareTo( rhs.Path );
|
||||||
|
|
||||||
public bool Equals( NewRelPath other )
|
public bool Equals( Utf8RelPath other )
|
||||||
=> Path.Equals( other.Path );
|
=> Path.Equals( other.Path );
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
|
|
@ -106,17 +111,17 @@ public readonly struct NewRelPath : IEquatable< NewRelPath >, IComparable< NewRe
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
=> Path.Dispose();
|
=> Path.Dispose();
|
||||||
|
|
||||||
private class NewRelPathConverter : JsonConverter
|
public class Utf8RelPathConverter : JsonConverter
|
||||||
{
|
{
|
||||||
public override bool CanConvert( Type objectType )
|
public override bool CanConvert( Type objectType )
|
||||||
=> objectType == typeof( NewRelPath );
|
=> objectType == typeof( Utf8RelPath );
|
||||||
|
|
||||||
public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer )
|
public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer )
|
||||||
{
|
{
|
||||||
var token = JToken.Load( reader ).ToString();
|
var token = JToken.Load( reader ).ToString();
|
||||||
return FromString( token, out var p )
|
return FromString( token, out var p )
|
||||||
? 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
|
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 )
|
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() );
|
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 );
|
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 )
|
public bool StartsWith( params char[] chars )
|
||||||
{
|
{
|
||||||
if( chars.Length > Length )
|
if( chars.Length > Length )
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ public class ModsController : WebApiController
|
||||||
public object GetFiles()
|
public object GetFiles()
|
||||||
{
|
{
|
||||||
return Penumbra.ModManager.Collections.CurrentCollection.Cache?.ResolvedFiles.ToDictionary(
|
return Penumbra.ModManager.Collections.CurrentCollection.Cache?.ResolvedFiles.ToDictionary(
|
||||||
o => ( string )o.Key,
|
o => o.Key.ToString(),
|
||||||
o => o.Value.FullName
|
o => o.Value.FullName
|
||||||
)
|
)
|
||||||
?? new Dictionary< string, string >();
|
?? new Dictionary< string, string >();
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ using System.Reflection;
|
||||||
using Dalamud.Game.ClientState.Objects.Types;
|
using Dalamud.Game.ClientState.Objects.Types;
|
||||||
using Dalamud.Logging;
|
using Dalamud.Logging;
|
||||||
using Lumina.Data;
|
using Lumina.Data;
|
||||||
|
using Penumbra.GameData.ByteString;
|
||||||
using Penumbra.GameData.Enums;
|
using Penumbra.GameData.Enums;
|
||||||
using Penumbra.GameData.Util;
|
using Penumbra.GameData.Util;
|
||||||
using Penumbra.Mods;
|
using Penumbra.Mods;
|
||||||
|
|
@ -78,16 +79,15 @@ public class PenumbraApi : IDisposable, IPenumbraApi
|
||||||
|
|
||||||
private static string ResolvePath( string path, ModManager manager, ModCollection collection )
|
private static string ResolvePath( string path, ModManager manager, ModCollection collection )
|
||||||
{
|
{
|
||||||
if( !Penumbra.Config.IsEnabled )
|
if( !Penumbra.Config.EnableMods )
|
||||||
{
|
{
|
||||||
return path;
|
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 );
|
var ret = collection.Cache?.ResolveSwappedOrReplacementPath( gamePath );
|
||||||
ret ??= manager.Collections.ForcedCollection.Cache?.ResolveSwappedOrReplacementPath( gamePath );
|
ret ??= manager.Collections.ForcedCollection.Cache?.ResolveSwappedOrReplacementPath( gamePath );
|
||||||
ret ??= path;
|
return ret?.ToString() ?? path;
|
||||||
return ret;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public string ResolvePath( string path )
|
public string ResolvePath( string path )
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Numerics;
|
|
||||||
using Dalamud.Configuration;
|
using Dalamud.Configuration;
|
||||||
using Dalamud.Logging;
|
using Dalamud.Logging;
|
||||||
|
|
||||||
|
|
@ -14,13 +12,19 @@ public class Configuration : IPluginConfiguration
|
||||||
|
|
||||||
public int Version { get; set; } = CurrentVersion;
|
public int Version { get; set; } = CurrentVersion;
|
||||||
|
|
||||||
public bool IsEnabled { get; set; } = true;
|
public bool EnableMods { get; set; } = true;
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
public bool DebugMode { get; set; } = true;
|
public bool DebugMode { get; set; } = true;
|
||||||
#else
|
#else
|
||||||
public bool DebugMode { get; set; } = false;
|
public bool DebugMode { get; set; } = false;
|
||||||
#endif
|
#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 ScaleModSelector { get; set; } = false;
|
||||||
|
|
||||||
public bool ShowAdvanced { get; set; }
|
public bool ShowAdvanced { get; set; }
|
||||||
|
|
||||||
public bool DisableFileSystemNotifications { get; set; }
|
public bool DisableFileSystemNotifications { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,15 @@ using Dalamud.Game.Gui;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using Dalamud.IoC;
|
using Dalamud.IoC;
|
||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
|
|
||||||
// ReSharper disable AutoPropertyCanBeMadeGetOnly.Local
|
// 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
|
// @formatter:off
|
||||||
[PluginService][RequiredVersion("1.0")] public static DalamudPluginInterface PluginInterface { get; private set; } = null!;
|
[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 TargetManager Targets { get; private set; } = null!;
|
||||||
[PluginService][RequiredVersion("1.0")] public static ObjectTable Objects { 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!;
|
[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 System.Collections.Generic;
|
||||||
using Penumbra.Structs;
|
using Penumbra.Mod;
|
||||||
|
|
||||||
namespace Penumbra.Importer.Models
|
namespace Penumbra.Importer.Models
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,10 @@ using System.Text;
|
||||||
using Dalamud.Logging;
|
using Dalamud.Logging;
|
||||||
using ICSharpCode.SharpZipLib.Zip;
|
using ICSharpCode.SharpZipLib.Zip;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
using Penumbra.GameData.ByteString;
|
||||||
using Penumbra.GameData.Util;
|
using Penumbra.GameData.Util;
|
||||||
using Penumbra.Importer.Models;
|
using Penumbra.Importer.Models;
|
||||||
using Penumbra.Mod;
|
using Penumbra.Mod;
|
||||||
using Penumbra.Structs;
|
|
||||||
using Penumbra.Util;
|
using Penumbra.Util;
|
||||||
using FileMode = System.IO.FileMode;
|
using FileMode = System.IO.FileMode;
|
||||||
|
|
||||||
|
|
@ -336,14 +336,18 @@ internal class TexToolsImport
|
||||||
{
|
{
|
||||||
OptionName = opt.Name!,
|
OptionName = opt.Name!,
|
||||||
OptionDesc = string.IsNullOrEmpty( opt.Description ) ? "" : opt.Description!,
|
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! );
|
var optDir = NewOptionDirectory( groupFolder, opt.Name! );
|
||||||
if( optDir.Exists )
|
if( optDir.Exists )
|
||||||
{
|
{
|
||||||
foreach( var file in optDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) )
|
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* OriginalResource;
|
||||||
public ResourceHandle* ManipulatedResource;
|
public ResourceHandle* ManipulatedResource;
|
||||||
public NewGamePath OriginalPath;
|
public Utf8GamePath OriginalPath;
|
||||||
public FullPath ManipulatedPath;
|
public FullPath ManipulatedPath;
|
||||||
public ResourceCategory Category;
|
public ResourceCategory Category;
|
||||||
public object? ResolverInfo;
|
public object? ResolverInfo;
|
||||||
|
|
@ -44,7 +44,7 @@ public unsafe partial class ResourceLoader
|
||||||
ResourceLoaded -= AddModifiedDebugInfo;
|
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 )
|
if( manipulatedPath == null )
|
||||||
{
|
{
|
||||||
|
|
@ -188,11 +188,11 @@ public unsafe partial class ResourceLoader
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logging functions for EnableLogging.
|
// Logging functions for EnableFullLogging.
|
||||||
private static void LogPath( NewGamePath path, bool synchronous )
|
private static void LogPath( Utf8GamePath path, bool synchronous )
|
||||||
=> PluginLog.Information( $"Requested {path} {( synchronous ? "synchronously." : "asynchronously." )}" );
|
=> 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();
|
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})" );
|
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 )
|
private static void LogLoadedFile( Utf8String path, bool success, bool custom )
|
||||||
=> PluginLog.Information( success
|
=> PluginLog.Information( success
|
||||||
? $"Loaded {path} from {( custom ? "local files" : "SqPack" )}"
|
? $"[ResourceLoader] Loaded {path} from {( custom ? "local files" : "SqPack" )}"
|
||||||
: $"Failed to load {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" )]
|
[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 )
|
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,
|
private ResourceHandle* GetResourceHandler( bool isSync, ResourceManager* resourceManager, ResourceCategory* categoryId, uint* resourceType,
|
||||||
int* resourceHash, byte* path, void* unk, bool isUnk )
|
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." );
|
PluginLog.Error( "Could not create GamePath from resource path." );
|
||||||
return CallOriginalHandler( isSync, resourceManager, categoryId, resourceType, resourceHash, path, unk, isUnk );
|
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 );
|
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;
|
byte ret;
|
||||||
// The internal buffer size does not allow for more than 260 characters.
|
// 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.
|
// 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.
|
// 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( path );
|
||||||
var resolved = Penumbra.ModManager.ResolveSwappedOrReplacementPath( gamePath );
|
return( resolved, null );
|
||||||
return resolved != null ? ( new FullPath( resolved ), null ) : ( null, null );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DisposeHooks()
|
private void DisposeHooks()
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,6 @@ using Dalamud.Hooking;
|
||||||
using Dalamud.Utility.Signatures;
|
using Dalamud.Utility.Signatures;
|
||||||
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
|
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
|
||||||
using Penumbra.GameData.ByteString;
|
using Penumbra.GameData.ByteString;
|
||||||
using Penumbra.GameData.Util;
|
|
||||||
using Penumbra.Util;
|
|
||||||
|
|
||||||
namespace Penumbra.Interop;
|
namespace Penumbra.Interop;
|
||||||
|
|
||||||
|
|
@ -69,7 +67,7 @@ public unsafe partial class ResourceLoader
|
||||||
: LoadMdlFileExternHook.Original( resourceHandle, unk1, unk2, ptr );
|
: 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 )
|
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.
|
// Events can be used to make smarter logging.
|
||||||
public bool IsLoggingEnabled { get; private set; }
|
public bool IsLoggingEnabled { get; private set; }
|
||||||
|
|
||||||
public void EnableLogging()
|
public void EnableFullLogging()
|
||||||
{
|
{
|
||||||
if( IsLoggingEnabled )
|
if( IsLoggingEnabled )
|
||||||
{
|
{
|
||||||
|
|
@ -31,7 +31,7 @@ public unsafe partial class ResourceLoader : IDisposable
|
||||||
EnableHooks();
|
EnableHooks();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DisableLogging()
|
public void DisableFullLogging()
|
||||||
{
|
{
|
||||||
if( !IsLoggingEnabled )
|
if( !IsLoggingEnabled )
|
||||||
{
|
{
|
||||||
|
|
@ -99,13 +99,13 @@ public unsafe partial class ResourceLoader : IDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
// Event fired whenever a resource is requested.
|
// 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;
|
public event ResourceRequestedDelegate? ResourceRequested;
|
||||||
|
|
||||||
// Event fired whenever a resource is returned.
|
// Event fired whenever a resource is returned.
|
||||||
// If the path was manipulated by penumbra, manipulatedPath will be the file path of the loaded resource.
|
// 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.
|
// 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 );
|
object? resolveData );
|
||||||
|
|
||||||
public event ResourceLoadedDelegate? ResourceLoaded;
|
public event ResourceLoadedDelegate? ResourceLoaded;
|
||||||
|
|
@ -118,10 +118,11 @@ public unsafe partial class ResourceLoader : IDisposable
|
||||||
public event FileLoadedDelegate? FileLoaded;
|
public event FileLoadedDelegate? FileLoaded;
|
||||||
|
|
||||||
// Customization point to control how path resolving is handled.
|
// 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()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
DisableFullLogging();
|
||||||
DisposeHooks();
|
DisposeHooks();
|
||||||
DisposeTexMdlTreatment();
|
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 System.Runtime.InteropServices;
|
||||||
using Penumbra.Structs;
|
|
||||||
|
|
||||||
namespace Penumbra.Interop.Structs;
|
namespace Penumbra.Interop.Structs;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ using Penumbra.GameData.ByteString;
|
||||||
using Penumbra.Importer;
|
using Penumbra.Importer;
|
||||||
using Penumbra.Meta.Files;
|
using Penumbra.Meta.Files;
|
||||||
using Penumbra.Mod;
|
using Penumbra.Mod;
|
||||||
using Penumbra.Structs;
|
|
||||||
using Penumbra.Util;
|
using Penumbra.Util;
|
||||||
|
|
||||||
namespace Penumbra.Meta;
|
namespace Penumbra.Meta;
|
||||||
|
|
@ -167,14 +166,14 @@ public class MetaCollection
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var path = new RelPath( file, basePath );
|
Utf8RelPath.FromFile( file, basePath, out var path );
|
||||||
var foundAny = false;
|
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;
|
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 Lumina.Data.Files;
|
||||||
using Penumbra.GameData.ByteString;
|
using Penumbra.GameData.ByteString;
|
||||||
using Penumbra.GameData.Util;
|
using Penumbra.GameData.Util;
|
||||||
using Penumbra.Interop;
|
|
||||||
using Penumbra.Meta.Files;
|
using Penumbra.Meta.Files;
|
||||||
using Penumbra.Util;
|
using Penumbra.Util;
|
||||||
|
|
||||||
|
|
@ -24,7 +23,7 @@ public class MetaManager : IDisposable
|
||||||
public FileInformation( object data )
|
public FileInformation( object data )
|
||||||
=> Data = data;
|
=> Data = data;
|
||||||
|
|
||||||
public void Write( DirectoryInfo dir, GamePath originalPath )
|
public void Write( DirectoryInfo dir, Utf8GamePath originalPath )
|
||||||
{
|
{
|
||||||
ByteData = Data switch
|
ByteData = Data switch
|
||||||
{
|
{
|
||||||
|
|
@ -44,16 +43,16 @@ public class MetaManager : IDisposable
|
||||||
|
|
||||||
public const string TmpDirectory = "penumbrametatmp";
|
public const string TmpDirectory = "penumbrametatmp";
|
||||||
|
|
||||||
private readonly DirectoryInfo _dir;
|
private readonly DirectoryInfo _dir;
|
||||||
private readonly Dictionary< GamePath, FullPath > _resolvedFiles;
|
private readonly Dictionary< Utf8GamePath, FullPath > _resolvedFiles;
|
||||||
|
|
||||||
private readonly Dictionary< MetaManipulation, Mod.Mod > _currentManipulations = new();
|
private readonly Dictionary< MetaManipulation, Mod.Mod > _currentManipulations = new();
|
||||||
private readonly Dictionary< GamePath, FileInformation > _currentFiles = new();
|
private readonly Dictionary< Utf8GamePath, FileInformation > _currentFiles = new();
|
||||||
|
|
||||||
public IEnumerable< (MetaManipulation, Mod.Mod) > Manipulations
|
public IEnumerable< (MetaManipulation, Mod.Mod) > Manipulations
|
||||||
=> _currentManipulations.Select( kvp => ( kvp.Key, kvp.Value ) );
|
=> _currentManipulations.Select( kvp => ( kvp.Key, kvp.Value ) );
|
||||||
|
|
||||||
public IEnumerable< (GamePath, FullPath) > Files
|
public IEnumerable< (Utf8GamePath, FullPath) > Files
|
||||||
=> _currentFiles.Where( kvp => kvp.Value.CurrentFile != null )
|
=> _currentFiles.Where( kvp => kvp.Value.CurrentFile != null )
|
||||||
.Select( kvp => ( kvp.Key, kvp.Value.CurrentFile!.Value ) );
|
.Select( kvp => ( kvp.Key, kvp.Value.CurrentFile!.Value ) );
|
||||||
|
|
||||||
|
|
@ -121,7 +120,7 @@ public class MetaManager : IDisposable
|
||||||
private void ClearDirectory()
|
private void ClearDirectory()
|
||||||
=> ClearDirectory( _dir );
|
=> ClearDirectory( _dir );
|
||||||
|
|
||||||
public MetaManager( string name, Dictionary< GamePath, FullPath > resolvedFiles, DirectoryInfo tempDir )
|
public MetaManager( string name, Dictionary< Utf8GamePath, FullPath > resolvedFiles, DirectoryInfo tempDir )
|
||||||
{
|
{
|
||||||
_resolvedFiles = resolvedFiles;
|
_resolvedFiles = resolvedFiles;
|
||||||
_dir = new DirectoryInfo( Path.Combine( tempDir.FullName, name.ReplaceBadXivSymbols() ) );
|
_dir = new DirectoryInfo( Path.Combine( tempDir.FullName, name.ReplaceBadXivSymbols() ) );
|
||||||
|
|
@ -135,13 +134,13 @@ public class MetaManager : IDisposable
|
||||||
Directory.CreateDirectory( _dir.FullName );
|
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 );
|
value.Write( _dir, key );
|
||||||
_resolvedFiles[ kvp.Key ] = kvp.Value.CurrentFile!.Value;
|
_resolvedFiles[ key ] = value.CurrentFile!.Value;
|
||||||
if( kvp.Value.Data is EqpFile )
|
if( value.Data is EqpFile )
|
||||||
{
|
{
|
||||||
EqpData = kvp.Value.ByteData;
|
EqpData = value.ByteData;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -154,7 +153,7 @@ public class MetaManager : IDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
_currentManipulations.Add( m, mod );
|
_currentManipulations.Add( m, mod );
|
||||||
var gamePath = m.CorrespondingFilename();
|
var gamePath = Utf8GamePath.FromString(m.CorrespondingFilename(), out var p, false) ? p : Utf8GamePath.Empty; // TODO
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if( !_currentFiles.TryGetValue( gamePath, out var file ) )
|
if( !_currentFiles.TryGetValue( gamePath, out var file ) )
|
||||||
|
|
|
||||||
|
|
@ -3,84 +3,81 @@ using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Dalamud.Logging;
|
using Dalamud.Logging;
|
||||||
using Dalamud.Plugin;
|
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using Penumbra.Mod;
|
using Penumbra.Mod;
|
||||||
using Penumbra.Mods;
|
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;
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
config.ModDirectory = config.CurrentCollection;
|
|
||||||
config.CurrentCollection = "Default";
|
|
||||||
config.DefaultCollection = "Default";
|
|
||||||
config.Version = 1;
|
|
||||||
ResettleCollectionJson( config );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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" ) );
|
return;
|
||||||
if( !collectionJson.Exists )
|
}
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var defaultCollection = new ModCollection();
|
var defaultCollection = new ModCollection();
|
||||||
var defaultCollectionFile = defaultCollection.FileName();
|
var defaultCollectionFile = defaultCollection.FileName();
|
||||||
if( defaultCollectionFile.Exists )
|
if( defaultCollectionFile.Exists )
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var text = File.ReadAllText( collectionJson.FullName );
|
var text = File.ReadAllText( collectionJson.FullName );
|
||||||
var data = JArray.Parse( text );
|
var data = JArray.Parse( text );
|
||||||
|
|
||||||
var maxPriority = 0;
|
var maxPriority = 0;
|
||||||
foreach( var setting in data.Cast< JObject >() )
|
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" ]!;
|
Enabled = enabled,
|
||||||
var enabled = ( bool )setting[ "Enabled" ]!;
|
Priority = priority,
|
||||||
var priority = ( int )setting[ "Priority" ]!;
|
Settings = settings!,
|
||||||
var settings = setting[ "Settings" ]!.ToObject< Dictionary< string, int > >()
|
};
|
||||||
?? setting[ "Conf" ]!.ToObject< Dictionary< string, int > >();
|
defaultCollection.Settings.Add( modName, save );
|
||||||
|
maxPriority = Math.Max( maxPriority, priority );
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
catch( Exception e )
|
|
||||||
|
if( !config.InvertModListOrder )
|
||||||
{
|
{
|
||||||
PluginLog.Error( $"Could not migrate the old collection file to new collection files:\n{e}" );
|
foreach( var setting in defaultCollection.Settings.Values )
|
||||||
throw;
|
{
|
||||||
|
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.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using Penumbra.GameData.Util;
|
using Penumbra.GameData.ByteString;
|
||||||
using Penumbra.Util;
|
|
||||||
|
|
||||||
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)
|
public ModSettings Settings { get; }
|
||||||
// and the resulting cache.
|
public ModData Data { get; }
|
||||||
public class Mod
|
public ModCache Cache { get; }
|
||||||
|
|
||||||
|
public Mod( ModSettings settings, ModData data )
|
||||||
{
|
{
|
||||||
public ModSettings Settings { get; }
|
Settings = settings;
|
||||||
public ModData Data { get; }
|
Data = data;
|
||||||
public ModCache Cache { get; }
|
Cache = new ModCache();
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Penumbra.GameData.Util;
|
using Penumbra.GameData.ByteString;
|
||||||
using Penumbra.Meta;
|
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 Dictionary< Mod, (List< Utf8GamePath > Files, List< MetaManipulation > Manipulations) > Conflicts { get; private set; } = new();
|
||||||
public class ModCache
|
|
||||||
|
public void AddConflict( Mod precedingMod, Utf8GamePath gamePath )
|
||||||
{
|
{
|
||||||
public Dictionary< Mod, (List< GamePath > Files, List< MetaManipulation > Manipulations) > Conflicts { get; private set; } = new();
|
if( Conflicts.TryGetValue( precedingMod, out var conflicts ) && !conflicts.Files.Contains( gamePath ) )
|
||||||
|
|
||||||
public void AddConflict( Mod precedingMod, GamePath gamePath )
|
|
||||||
{
|
{
|
||||||
if( Conflicts.TryGetValue( precedingMod, out var conflicts ) && !conflicts.Files.Contains( gamePath ) )
|
conflicts.Files.Add( gamePath );
|
||||||
{
|
|
||||||
conflicts.Files.Add( gamePath );
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Conflicts[ precedingMod ] = ( new List< GamePath > { gamePath }, new List< MetaManipulation >() );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
public void AddConflict( Mod precedingMod, MetaManipulation manipulation )
|
|
||||||
{
|
{
|
||||||
if( Conflicts.TryGetValue( precedingMod, out var conflicts ) && !conflicts.Manipulations.Contains( manipulation ) )
|
Conflicts[ precedingMod ] = ( new List< Utf8GamePath > { gamePath }, new List< MetaManipulation >() );
|
||||||
{
|
|
||||||
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;
|
|
||||||
} );
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using Dalamud.Logging;
|
using Dalamud.Logging;
|
||||||
|
using Penumbra.GameData.ByteString;
|
||||||
using Penumbra.GameData.Util;
|
using Penumbra.GameData.Util;
|
||||||
using Penumbra.Importer;
|
using Penumbra.Importer;
|
||||||
using Penumbra.Mods;
|
using Penumbra.Mods;
|
||||||
using Penumbra.Structs;
|
|
||||||
using Penumbra.Util;
|
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";
|
_hasher ??= SHA256.Create();
|
||||||
private const string Required = "Required";
|
return _hasher;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ModCleanup( DirectoryInfo baseDir, ModMeta mod )
|
||||||
|
{
|
||||||
|
_baseDir = baseDir;
|
||||||
|
_mod = mod;
|
||||||
|
BuildDict();
|
||||||
|
}
|
||||||
|
|
||||||
private readonly DirectoryInfo _baseDir;
|
private void BuildDict()
|
||||||
private readonly ModMeta _mod;
|
{
|
||||||
private SHA256? _hasher;
|
foreach( var file in _baseDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) )
|
||||||
|
|
||||||
private readonly Dictionary< long, List< FileInfo > > _filesBySize = new();
|
|
||||||
|
|
||||||
private SHA256 Sha()
|
|
||||||
{
|
{
|
||||||
_hasher ??= SHA256.Create();
|
var fileLength = file.Length;
|
||||||
return _hasher;
|
if( _filesBySize.TryGetValue( fileLength, out var files ) )
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
files.Add( file );
|
||||||
if( _filesBySize.TryGetValue( fileLength, out var files ) )
|
}
|
||||||
{
|
else
|
||||||
files.Add( file );
|
{
|
||||||
}
|
_filesBySize[ fileLength ] = new List< FileInfo > { file };
|
||||||
else
|
|
||||||
{
|
|
||||||
_filesBySize[ fileLength ] = new List< FileInfo >() { file };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static DirectoryInfo CreateNewModDir( ModData mod, string optionGroup, string option )
|
private static DirectoryInfo CreateNewModDir( ModData mod, string optionGroup, string option )
|
||||||
{
|
{
|
||||||
var newName = $"{mod.BasePath.Name}_{optionGroup}_{option}";
|
var newName = $"{mod.BasePath.Name}_{optionGroup}_{option}";
|
||||||
var newDir = TexToolsImport.CreateModFolder( new DirectoryInfo( Penumbra.Config!.ModDirectory ), newName );
|
return TexToolsImport.CreateModFolder( new DirectoryInfo( Penumbra.Config.ModDirectory ), newName );
|
||||||
return newDir;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private static ModData CreateNewMod( DirectoryInfo newDir, string newSortOrder )
|
private static ModData CreateNewMod( DirectoryInfo newDir, string newSortOrder )
|
||||||
{
|
{
|
||||||
Penumbra.ModManager.AddMod( newDir );
|
Penumbra.ModManager.AddMod( newDir );
|
||||||
var newMod = Penumbra.ModManager.Mods[ newDir.Name ];
|
var newMod = Penumbra.ModManager.Mods[ newDir.Name ];
|
||||||
newMod.Move( newSortOrder );
|
newMod.Move( newSortOrder );
|
||||||
newMod.ComputeChangedItems();
|
newMod.ComputeChangedItems();
|
||||||
ModFileSystem.InvokeChange();
|
ModFileSystem.InvokeChange();
|
||||||
return newMod;
|
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,
|
var oldPath = Path.Combine( mod.BasePath.FullName, fileName.ToString() );
|
||||||
Name = name,
|
unseenPaths.Remove( oldPath );
|
||||||
Description = $"Split from {mod.Meta.Name} Group {optionGroup} Option {option}.",
|
if( File.Exists( oldPath ) )
|
||||||
};
|
|
||||||
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 );
|
foreach( var path in paths )
|
||||||
unseenPaths.Remove( oldPath );
|
|
||||||
if( File.Exists( oldPath ) )
|
|
||||||
{
|
{
|
||||||
foreach( var path in paths )
|
var newPath = Path.Combine( newDir.FullName, path.ToString() );
|
||||||
{
|
Directory.CreateDirectory( Path.GetDirectoryName( newPath )! );
|
||||||
var newPath = Path.Combine( newDir.FullName, path );
|
File.Copy( oldPath, newPath, true );
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CleanUpDuplicates( mod );
|
var newSortOrder = group.SelectionType == SelectType.Single
|
||||||
ClearEmptySubDirectories( dedup._baseDir );
|
? $"{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 );
|
foreach( var option in group.Options )
|
||||||
RelPath relName2 = new( f2, _baseDir );
|
|
||||||
|
|
||||||
var inOption1 = false;
|
|
||||||
var inOption2 = false;
|
|
||||||
foreach( var option in _mod.Groups.SelectMany( g => g.Value.Options ) )
|
|
||||||
{
|
{
|
||||||
if( option.OptionFiles.ContainsKey( relName1 ) )
|
CreateModSplit( unseenPaths, mod, group, option );
|
||||||
{
|
|
||||||
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, RelPath relPath, bool exceptDuplicates = false )
|
if( unseenPaths.Count == 0 )
|
||||||
{
|
{
|
||||||
var groupEnumerator = exceptDuplicates
|
return;
|
||||||
? 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 )
|
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 );
|
duplicates.Options.Add( RequiredOption() );
|
||||||
if( requiredIdx >= 0 )
|
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 ];
|
if( CompareFilesDirectly( value[ 0 ], value[ 1 ] ) )
|
||||||
foreach( var kvp in required.OptionFiles.ToArray() )
|
|
||||||
{
|
{
|
||||||
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;
|
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,
|
return;
|
||||||
Single = 1,
|
}
|
||||||
Multi = 2,
|
|
||||||
|
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" ),
|
||||||
};
|
};
|
||||||
|
foreach( var group in enumerator )
|
||||||
private static void RemoveFromGroups( ModMeta meta, RelPath relPath, GamePath gamePath, GroupType type = GroupType.Both,
|
|
||||||
bool skipDuplicates = true )
|
|
||||||
{
|
{
|
||||||
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;
|
if( option.OptionFiles.TryGetValue( relPath, out var gamePaths ) && gamePaths.Remove( gamePath ) && gamePaths.Count == 0 )
|
||||||
}
|
|
||||||
|
|
||||||
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 )
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try
|
||||||
private static void RemoveUselessGroups( ModMeta meta )
|
|
||||||
{
|
{
|
||||||
meta.Groups = meta.Groups.Where( kvp => kvp.Value.Options.Any( o => o.OptionFiles.Count > 0 ) )
|
var newFullPath = Path.Combine( basePath, newRelPath.ToString() );
|
||||||
.ToDictionary( kvp => kvp.Key, kvp => kvp.Value );
|
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.
|
foreach( var option in meta.Groups.Values.SelectMany( group => group.Options ) )
|
||||||
// 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 ) )
|
if( option.OptionFiles.TryGetValue( oldRelPath, out var gamePaths ) )
|
||||||
{
|
{
|
||||||
var firstOption = true;
|
option.OptionFiles.Add( newRelPath, gamePaths );
|
||||||
HashSet< (RelPath, GamePath) > groupList = new();
|
option.OptionFiles.Remove( oldRelPath );
|
||||||
foreach( var option in group.Options )
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
optionList.UnionWith( gamePaths.Select( p => ( file, p ) ) );
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var newPath = new Dictionary< RelPath, GamePath >();
|
if( firstOption )
|
||||||
foreach( var (path, gamePath) in groupList )
|
|
||||||
{
|
{
|
||||||
var relPath = new RelPath( gamePath );
|
groupList = optionList;
|
||||||
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 );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
groupList.IntersectWith( optionList );
|
||||||
|
}
|
||||||
|
|
||||||
|
firstOption = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
RemoveUselessGroups( meta );
|
var newPath = new Dictionary< Utf8RelPath, Utf8GamePath >();
|
||||||
ClearEmptySubDirectories( baseDir );
|
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();
|
var group = new OptionGroup
|
||||||
ClearEmptySubDirectories( baseDir );
|
|
||||||
foreach( var groupDir in baseDir.EnumerateDirectories() )
|
|
||||||
{
|
{
|
||||||
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,
|
OptionDesc = string.Empty,
|
||||||
SelectionType = SelectType.Single,
|
OptionName = optionDir.Name,
|
||||||
Options = new List< Option >(),
|
OptionFiles = new Dictionary< Utf8RelPath, HashSet< Utf8GamePath > >(),
|
||||||
};
|
};
|
||||||
|
foreach( var file in optionDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) )
|
||||||
foreach( var optionDir in groupDir.EnumerateDirectories() )
|
|
||||||
{
|
{
|
||||||
var option = new Option
|
if( Utf8RelPath.FromFile( file, baseDir, out var rel )
|
||||||
|
&& Utf8GamePath.FromFile( file, optionDir, out var game ) )
|
||||||
{
|
{
|
||||||
OptionDesc = string.Empty,
|
option.OptionFiles[ rel ] = new HashSet< Utf8GamePath > { game };
|
||||||
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 );
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
if( group.Options.Any() )
|
||||||
collection.UpdateSetting(baseDir, meta, true);
|
{
|
||||||
|
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.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Dalamud.Logging;
|
using Dalamud.Logging;
|
||||||
|
using Penumbra.GameData.ByteString;
|
||||||
using Penumbra.Mods;
|
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; }
|
get => _sortOrderName;
|
||||||
|
set => _sortOrderName = value.Replace( '/', '\\' );
|
||||||
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 );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ModData contains all permanent information about a mod,
|
public string SortOrderPath
|
||||||
// and is independent of collections or settings.
|
=> ParentFolder.FullName;
|
||||||
// It only changes when the user actively changes the mod or their filesystem.
|
|
||||||
public class ModData
|
public string FullName
|
||||||
{
|
{
|
||||||
public DirectoryInfo BasePath;
|
get
|
||||||
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;
|
var path = SortOrderPath;
|
||||||
Meta = meta;
|
return path.Length > 0 ? $"{path}/{SortOrderName}" : SortOrderName;
|
||||||
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 => 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.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Penumbra.GameData.Util;
|
using Penumbra.GameData.ByteString;
|
||||||
using Penumbra.Structs;
|
|
||||||
using Penumbra.Util;
|
|
||||||
|
|
||||||
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 bool CleanUpCollection( Dictionary< string, ModSettings > settings, IEnumerable< DirectoryInfo > modPaths )
|
||||||
public static class ModFunctions
|
|
||||||
{
|
{
|
||||||
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();
|
anyChanges |= settings.Remove( toRemove );
|
||||||
var missingMods = settings.Keys.Where( k => !hashes.Contains( k ) ).ToArray();
|
|
||||||
var anyChanges = false;
|
|
||||||
foreach( var toRemove in missingMods )
|
|
||||||
{
|
|
||||||
anyChanges |= settings.Remove( toRemove );
|
|
||||||
}
|
|
||||||
|
|
||||||
return anyChanges;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
doNotAdd |= group.ApplyGroupFiles( relPath, settings.Settings[ group.GroupName ], files );
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static HashSet< GamePath > GetAllFiles( RelPath relPath, ModMeta meta )
|
if( !doNotAdd )
|
||||||
{
|
{
|
||||||
var ret = new HashSet< GamePath >();
|
files.Add( relPath.ToGamePath() );
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
ret.UnionWith( files );
|
||||||
Settings = namedSettings.Settings.Keys.ToDictionary( k => k, _ => 0 ),
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
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[ setting ] = 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
foreach( var idx in namedSettings.Settings[ kvp.Key ]
|
var idx = info.Options.FindIndex( o => o.OptionName == namedSettings.Settings[ setting ].Last() );
|
||||||
.Select( option => info.Options.FindIndex( o => o.OptionName == option ) )
|
ret.Settings[ setting ] = idx < 0 ? 0 : idx;
|
||||||
.Where( idx => idx >= 0 ) )
|
}
|
||||||
{
|
}
|
||||||
ret.Settings[ kvp.Key ] |= 1 << 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 System.Linq;
|
||||||
using Dalamud.Logging;
|
using Dalamud.Logging;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
using Penumbra.GameData.ByteString;
|
||||||
using Penumbra.GameData.Util;
|
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 uint FileVersion { get; set; }
|
||||||
public class ModMeta
|
|
||||||
|
public string Name
|
||||||
{
|
{
|
||||||
public uint FileVersion { get; set; }
|
get => _name;
|
||||||
|
set
|
||||||
public string Name
|
|
||||||
{
|
{
|
||||||
get => _name;
|
_name = value;
|
||||||
set
|
LowerName = value.ToLowerInvariant();
|
||||||
{
|
|
||||||
_name = value;
|
|
||||||
LowerName = value.ToLowerInvariant();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private string _name = "Mod";
|
private string _name = "Mod";
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public string LowerName { get; private set; } = "mod";
|
public string LowerName { get; private set; } = "mod";
|
||||||
|
|
||||||
private string _author = "";
|
private string _author = "";
|
||||||
|
|
||||||
public string Author
|
public string Author
|
||||||
|
{
|
||||||
|
get => _author;
|
||||||
|
set
|
||||||
{
|
{
|
||||||
get => _author;
|
_author = value;
|
||||||
set
|
LowerAuthor = value.ToLowerInvariant();
|
||||||
{
|
|
||||||
_author = value;
|
|
||||||
LowerAuthor = value.ToLowerInvariant();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public string LowerAuthor { get; private set; } = "";
|
public string LowerAuthor { get; private set; } = "";
|
||||||
|
|
||||||
public string Description { get; set; } = "";
|
public string Description { get; set; } = "";
|
||||||
public string Version { get; set; } = "";
|
public string Version { get; set; } = "";
|
||||||
public string Website { get; set; } = "";
|
public string Website { get; set; } = "";
|
||||||
|
|
||||||
[JsonProperty( ItemConverterType = typeof( GamePathConverter ) )]
|
[JsonProperty( ItemConverterType = typeof( FullPath.FullPathConverter ) )]
|
||||||
public Dictionary< GamePath, GamePath > FileSwaps { get; set; } = new();
|
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]
|
[JsonIgnore]
|
||||||
private int FileHash { get; set; }
|
private int FileHash { get; set; }
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public bool HasGroupsWithConfig { get; private set; }
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ModMeta? LoadFromFile( FileInfo filePath )
|
if( newMeta.FileHash == FileHash )
|
||||||
{
|
{
|
||||||
try
|
return false;
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
var text = File.ReadAllText( filePath.FullName );
|
||||||
HasGroupsWithConfig = Groups.Values.Any( g => g.Options.Count > 1 || g.SelectionType == SelectType.Multi && g.Options.Count == 1 );
|
|
||||||
return oldValue != HasGroupsWithConfig;
|
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 )
|
||||||
|
|
||||||
public void SaveToFile( FileInfo filePath )
|
|
||||||
{
|
{
|
||||||
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 );
|
File.WriteAllText( filePath.FullName, text );
|
||||||
var newHash = text.GetHashCode();
|
FileHash = newHash;
|
||||||
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}" );
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
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 bool Enabled { get; set; }
|
||||||
public class ModSettings
|
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; }
|
set => Settings = value;
|
||||||
public int Priority { get; set; }
|
}
|
||||||
public Dictionary< string, int > Settings { get; set; } = new();
|
|
||||||
|
|
||||||
// For backwards compatibility
|
public ModSettings DeepCopy()
|
||||||
private Dictionary< string, int > Conf
|
{
|
||||||
|
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,
|
SelectType.Single => Math.Min( Math.Max( oldSetting, 0 ), group.Options.Count - 1 ),
|
||||||
Priority = Priority,
|
SelectType.Multi => Math.Min( Math.Max( oldSetting, 0 ), ( 1 << group.Options.Count ) - 1 ),
|
||||||
Settings = Settings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value ),
|
_ => 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()
|
return false;
|
||||||
{
|
|
||||||
Enabled = false,
|
|
||||||
Priority = 0,
|
|
||||||
Settings = meta.Groups.ToDictionary( kvp => kvp.Key, _ => 0 ),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool FixSpecificSetting( string name, ModMeta meta )
|
return Settings.Keys.ToArray().Union( meta.Groups.Keys )
|
||||||
{
|
.Aggregate( false, ( current, name ) => current | FixSpecificSetting( name, 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 ) );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,45 +1,43 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
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.
|
public int Priority { get; set; }
|
||||||
// This is meant to make them possibly more portable when we support importing collections from other users.
|
public Dictionary< string, HashSet< string > > Settings { get; set; } = new();
|
||||||
// Enabled does not exist, because disabled mods would not be exported in this way.
|
|
||||||
public class NamedModSettings
|
public void AddFromModSetting( ModSettings s, ModMeta meta )
|
||||||
{
|
{
|
||||||
public int Priority { get; set; }
|
Priority = s.Priority;
|
||||||
public Dictionary< string, HashSet< string > > Settings { get; set; } = new();
|
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;
|
if( !meta.Groups.TryGetValue( kvp.Key, out var info ) )
|
||||||
Settings = s.Settings.Keys.ToDictionary( k => k, _ => new HashSet< string >() );
|
|
||||||
|
|
||||||
foreach( var kvp in Settings )
|
|
||||||
{
|
{
|
||||||
if( !meta.Groups.TryGetValue( kvp.Key, out var info ) )
|
continue;
|
||||||
{
|
}
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var setting = s.Settings[ kvp.Key ];
|
var setting = s.Settings[ kvp.Key ];
|
||||||
if( info.SelectionType == SelectType.Single )
|
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
|
if( ( ( setting >> i ) & 1 ) != 0 )
|
||||||
? 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 )
|
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.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Dalamud.Logging;
|
using Dalamud.Logging;
|
||||||
using Penumbra.Interop;
|
|
||||||
using Penumbra.Mod;
|
using Penumbra.Mod;
|
||||||
using Penumbra.Util;
|
using Penumbra.Util;
|
||||||
|
|
||||||
|
|
@ -250,16 +249,16 @@ public class CollectionManager
|
||||||
|
|
||||||
public bool CreateCharacterCollection( string characterName )
|
public bool CreateCharacterCollection( string characterName )
|
||||||
{
|
{
|
||||||
if( !CharacterCollection.ContainsKey( characterName ) )
|
if( CharacterCollection.ContainsKey( characterName ) )
|
||||||
{
|
{
|
||||||
CharacterCollection[ characterName ] = ModCollection.Empty;
|
return false;
|
||||||
Penumbra.Config.CharacterCollections[ characterName ] = string.Empty;
|
|
||||||
Penumbra.Config.Save();
|
|
||||||
Penumbra.PlayerWatcher.AddPlayerToWatch( characterName );
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 )
|
public void RemoveCharacterCollection( string characterName )
|
||||||
|
|
@ -299,7 +298,7 @@ public class CollectionManager
|
||||||
|
|
||||||
private bool LoadForcedCollection( Configuration config )
|
private bool LoadForcedCollection( Configuration config )
|
||||||
{
|
{
|
||||||
if( config.ForcedCollection == string.Empty )
|
if( config.ForcedCollection.Length == 0 )
|
||||||
{
|
{
|
||||||
ForcedCollection = ModCollection.Empty;
|
ForcedCollection = ModCollection.Empty;
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -320,7 +319,7 @@ public class CollectionManager
|
||||||
|
|
||||||
private bool LoadDefaultCollection( Configuration config )
|
private bool LoadDefaultCollection( Configuration config )
|
||||||
{
|
{
|
||||||
if( config.DefaultCollection == string.Empty )
|
if( config.DefaultCollection.Length == 0 )
|
||||||
{
|
{
|
||||||
DefaultCollection = ModCollection.Empty;
|
DefaultCollection = ModCollection.Empty;
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -345,7 +344,7 @@ public class CollectionManager
|
||||||
foreach( var (player, collectionName) in config.CharacterCollections.ToArray() )
|
foreach( var (player, collectionName) in config.CharacterCollections.ToArray() )
|
||||||
{
|
{
|
||||||
Penumbra.PlayerWatcher.AddPlayerToWatch( player );
|
Penumbra.PlayerWatcher.AddPlayerToWatch( player );
|
||||||
if( collectionName == string.Empty )
|
if( collectionName.Length == 0 )
|
||||||
{
|
{
|
||||||
CharacterCollection.Add( player, ModCollection.Empty );
|
CharacterCollection.Add( player, ModCollection.Empty );
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,255 +1,256 @@
|
||||||
using Dalamud.Plugin;
|
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Dalamud.Logging;
|
using Dalamud.Logging;
|
||||||
|
using Penumbra.GameData.ByteString;
|
||||||
using Penumbra.GameData.Util;
|
using Penumbra.GameData.Util;
|
||||||
using Penumbra.Interop;
|
|
||||||
using Penumbra.Mod;
|
using Penumbra.Mod;
|
||||||
using Penumbra.Util;
|
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.
|
public const string DefaultCollection = "Default";
|
||||||
// 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.
|
public string Name { get; set; }
|
||||||
// Active ModCollections build a cache of currently relevant data.
|
|
||||||
public class ModCollection
|
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 Mod.Mod GetMod( ModData mod )
|
||||||
|
{
|
||||||
public ModCollection()
|
if( Cache != null && Cache.AvailableMods.TryGetValue( mod.BasePath.Name, out var ret ) )
|
||||||
{
|
{
|
||||||
Name = DefaultCollection;
|
return ret;
|
||||||
Settings = new Dictionary< string, ModSettings >();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ModCollection( string name, Dictionary< string, ModSettings > settings )
|
if( Settings.TryGetValue( mod.BasePath.Name, out var settings ) )
|
||||||
{
|
{
|
||||||
Name = name;
|
return new Mod.Mod( settings, mod );
|
||||||
Settings = settings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value.DeepCopy() );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Mod.Mod GetMod( ModData mod )
|
var newSettings = ModSettings.DefaultSettings( mod.Meta );
|
||||||
{
|
Settings.Add( mod.BasePath.Name, newSettings );
|
||||||
if( Cache != null && Cache.AvailableMods.TryGetValue( mod.BasePath.Name, out var ret ) )
|
Save();
|
||||||
{
|
return new Mod.Mod( newSettings, mod );
|
||||||
return ret;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
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 ) )
|
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 );
|
if( changedSettings )
|
||||||
Settings.Add( mod.BasePath.Name, newSettings );
|
{
|
||||||
Save();
|
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();
|
return;
|
||||||
|
|
||||||
foreach( var s in removeList )
|
|
||||||
{
|
|
||||||
Settings.Remove( s.Key );
|
|
||||||
}
|
|
||||||
|
|
||||||
return removeList.Length > 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void CreateCache( DirectoryInfo modDirectory, IEnumerable< ModData > data )
|
if( clear )
|
||||||
{
|
{
|
||||||
Cache = new ModCollectionCache( Name, modDirectory );
|
settings.Settings.Clear();
|
||||||
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 );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ClearCache()
|
if( settings.FixInvalidSettings( meta ) )
|
||||||
=> Cache = null;
|
|
||||||
|
|
||||||
public void UpdateSetting( DirectoryInfo modPath, ModMeta meta, bool clear )
|
|
||||||
{
|
{
|
||||||
if( !Settings.TryGetValue( modPath.Name, out var settings ) )
|
Save();
|
||||||
{
|
}
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (clear)
|
public void UpdateSetting( ModData mod )
|
||||||
settings.Settings.Clear();
|
=> UpdateSetting( mod.BasePath, mod.Meta, false );
|
||||||
if( settings.FixInvalidSettings( meta ) )
|
|
||||||
{
|
public void UpdateSettings( bool forceSave )
|
||||||
Save();
|
{
|
||||||
}
|
if( Cache == null )
|
||||||
|
{
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateSetting( ModData mod )
|
var changes = false;
|
||||||
=> UpdateSetting( mod.BasePath, mod.Meta, false );
|
foreach( var mod in Cache.AvailableMods.Values )
|
||||||
|
|
||||||
public void UpdateSettings( bool forceSave )
|
|
||||||
{
|
{
|
||||||
if( Cache == null )
|
changes |= mod.FixSettings();
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var changes = false;
|
|
||||||
foreach( var mod in Cache.AvailableMods.Values )
|
|
||||||
{
|
|
||||||
changes |= mod.FixSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
if( forceSave || changes )
|
|
||||||
{
|
|
||||||
Save();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void CalculateEffectiveFileList( DirectoryInfo modDir, bool withMetaManipulations, bool activeCollection )
|
if( forceSave || changes )
|
||||||
{
|
{
|
||||||
PluginLog.Debug( "Recalculating effective file list for {CollectionName} [{WithMetaManipulations}] [{IsActiveCollection}]", Name,
|
Save();
|
||||||
withMetaManipulations, activeCollection );
|
}
|
||||||
Cache ??= new ModCollectionCache( Name, modDir );
|
}
|
||||||
UpdateSettings( false );
|
|
||||||
Cache.CalculateEffectiveFileList();
|
public void CalculateEffectiveFileList( DirectoryInfo modDir, bool withMetaManipulations, bool activeCollection )
|
||||||
if( withMetaManipulations )
|
{
|
||||||
|
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();
|
Penumbra.ResidentResources.Reload();
|
||||||
if( activeCollection )
|
|
||||||
{
|
|
||||||
Penumbra.ResidentResources.Reload();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public ModCollectionCache? Cache { get; private set; }
|
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." );
|
||||||
{
|
|
||||||
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}" );
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SaveToFile( FileInfo file )
|
try
|
||||||
{
|
{
|
||||||
try
|
var collection = JsonConvert.DeserializeObject< ModCollection >( File.ReadAllText( file.FullName ) );
|
||||||
{
|
return collection;
|
||||||
File.WriteAllText( file.FullName, JsonConvert.SerializeObject( this, Formatting.Indented ) );
|
}
|
||||||
}
|
catch( Exception e )
|
||||||
catch( Exception e )
|
{
|
||||||
{
|
PluginLog.Error( $"Could not read collection information from {file.FullName}:\n{e}" );
|
||||||
PluginLog.Error( $"Could not write collection {Name} to {file.FullName}:\n{e}" );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static DirectoryInfo CollectionDir()
|
return null;
|
||||||
=> 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 = "" };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.GameData.Util;
|
||||||
using Penumbra.Meta;
|
using Penumbra.Meta;
|
||||||
using Penumbra.Mod;
|
using Penumbra.Mod;
|
||||||
using Penumbra.Structs;
|
|
||||||
using Penumbra.Util;
|
using Penumbra.Util;
|
||||||
|
|
||||||
namespace Penumbra.Mods;
|
namespace Penumbra.Mods;
|
||||||
|
|
@ -20,17 +19,16 @@ namespace Penumbra.Mods;
|
||||||
public class ModCollectionCache
|
public class ModCollectionCache
|
||||||
{
|
{
|
||||||
// Shared caches to avoid allocations.
|
// Shared caches to avoid allocations.
|
||||||
private static readonly BitArray FileSeen = new(256);
|
private static readonly BitArray FileSeen = new(256);
|
||||||
private static readonly Dictionary< GamePath, Mod.Mod > RegisteredFiles = new(256);
|
private static readonly Dictionary< Utf8GamePath, Mod.Mod > RegisteredFiles = new(256);
|
||||||
|
|
||||||
public readonly Dictionary< string, Mod.Mod > AvailableMods = new();
|
public readonly Dictionary< string, Mod.Mod > AvailableMods = new();
|
||||||
|
|
||||||
private readonly SortedList< string, object? > _changedItems = new();
|
private readonly SortedList< string, object? > _changedItems = new();
|
||||||
public readonly Dictionary< GamePath, FullPath > ResolvedFiles = new();
|
public readonly Dictionary< Utf8GamePath, FullPath > ResolvedFiles = new();
|
||||||
public readonly Dictionary< GamePath, GamePath > SwappedFiles = new();
|
public readonly HashSet< FullPath > MissingFiles = new();
|
||||||
public readonly HashSet< FullPath > MissingFiles = new();
|
public readonly HashSet< ulong > Checksums = new();
|
||||||
public readonly HashSet< ulong > Checksums = new();
|
public readonly MetaManager MetaManipulations;
|
||||||
public readonly MetaManager MetaManipulations;
|
|
||||||
|
|
||||||
public IReadOnlyDictionary< string, object? > ChangedItems
|
public IReadOnlyDictionary< string, object? > ChangedItems
|
||||||
{
|
{
|
||||||
|
|
@ -61,7 +59,6 @@ public class ModCollectionCache
|
||||||
public void CalculateEffectiveFileList()
|
public void CalculateEffectiveFileList()
|
||||||
{
|
{
|
||||||
ResolvedFiles.Clear();
|
ResolvedFiles.Clear();
|
||||||
SwappedFiles.Clear();
|
|
||||||
MissingFiles.Clear();
|
MissingFiles.Clear();
|
||||||
RegisteredFiles.Clear();
|
RegisteredFiles.Clear();
|
||||||
_changedItems.Clear();
|
_changedItems.Clear();
|
||||||
|
|
@ -85,7 +82,7 @@ public class ModCollectionCache
|
||||||
|
|
||||||
private void SetChangedItems()
|
private void SetChangedItems()
|
||||||
{
|
{
|
||||||
if( _changedItems.Count > 0 || ResolvedFiles.Count + SwappedFiles.Count + MetaManipulations.Count == 0 )
|
if( _changedItems.Count > 0 || ResolvedFiles.Count + MetaManipulations.Count == 0 )
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -98,12 +95,7 @@ public class ModCollectionCache
|
||||||
var identifier = GameData.GameData.GetIdentifier();
|
var identifier = GameData.GameData.GetIdentifier();
|
||||||
foreach( var resolved in ResolvedFiles.Keys.Where( file => !metaFiles.Contains( file ) ) )
|
foreach( var resolved in ResolvedFiles.Keys.Where( file => !metaFiles.Contains( file ) ) )
|
||||||
{
|
{
|
||||||
identifier.Identify( _changedItems, resolved );
|
identifier.Identify( _changedItems, resolved.ToGamePath() );
|
||||||
}
|
|
||||||
|
|
||||||
foreach( var swapped in SwappedFiles.Keys )
|
|
||||||
{
|
|
||||||
identifier.Identify( _changedItems, swapped );
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch( Exception e )
|
catch( Exception e )
|
||||||
|
|
@ -134,12 +126,12 @@ public class ModCollectionCache
|
||||||
AddRemainingFiles( mod );
|
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,
|
// If audio streaming is not disabled, replacing .scd files crashes the game,
|
||||||
// so only add those files if it is disabled.
|
// so only add those files if it is disabled.
|
||||||
if( !Penumbra.Config.DisableSoundStreaming
|
if( !Penumbra.Config.DisableSoundStreaming
|
||||||
&& gamePath.ToString().EndsWith( ".scd", StringComparison.InvariantCultureIgnoreCase ) )
|
&& gamePath.Path.EndsWith( '.', 's', 'c', 'd' ) )
|
||||||
{
|
{
|
||||||
return true;
|
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 ) )
|
if( FilterFile( gamePath ) )
|
||||||
{
|
{
|
||||||
|
|
@ -187,9 +179,8 @@ public class ModCollectionCache
|
||||||
{
|
{
|
||||||
foreach( var (file, paths) in option.OptionFiles )
|
foreach( var (file, paths) in option.OptionFiles )
|
||||||
{
|
{
|
||||||
var fullPath = new FullPath( mod.Data.BasePath,
|
var fullPath = new FullPath( mod.Data.BasePath, file );
|
||||||
NewRelPath.FromString( file.ToString(), out var p ) ? p : NewRelPath.Empty ); // TODO
|
var idx = mod.Data.Resources.ModFiles.IndexOf( f => f.Equals( fullPath ) );
|
||||||
var idx = mod.Data.Resources.ModFiles.IndexOf( f => f.Equals( fullPath ) );
|
|
||||||
if( idx < 0 )
|
if( idx < 0 )
|
||||||
{
|
{
|
||||||
AddMissingFile( fullPath );
|
AddMissingFile( fullPath );
|
||||||
|
|
@ -259,7 +250,7 @@ public class ModCollectionCache
|
||||||
{
|
{
|
||||||
if( file.ToGamePath( mod.Data.BasePath, out var gamePath ) )
|
if( file.ToGamePath( mod.Data.BasePath, out var gamePath ) )
|
||||||
{
|
{
|
||||||
AddFile( mod, new GamePath( gamePath.ToString() ), file ); // TODO
|
AddFile( mod, gamePath, file );
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -294,7 +285,7 @@ public class ModCollectionCache
|
||||||
if( !RegisteredFiles.TryGetValue( key, out var oldMod ) )
|
if( !RegisteredFiles.TryGetValue( key, out var oldMod ) )
|
||||||
{
|
{
|
||||||
RegisteredFiles.Add( key, mod );
|
RegisteredFiles.Add( key, mod );
|
||||||
SwappedFiles.Add( key, value );
|
ResolvedFiles.Add( key, value );
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -341,54 +332,54 @@ public class ModCollectionCache
|
||||||
|
|
||||||
public void RemoveMod( DirectoryInfo basePath )
|
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 );
|
return;
|
||||||
if( mod.Settings.Enabled )
|
}
|
||||||
{
|
|
||||||
CalculateEffectiveFileList();
|
AvailableMods.Remove( basePath.Name );
|
||||||
if( mod.Data.Resources.MetaManipulations.Count > 0 )
|
if( !mod.Settings.Enabled )
|
||||||
{
|
{
|
||||||
UpdateMetaManipulations();
|
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 )
|
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 );
|
return;
|
||||||
AvailableMods[ data.BasePath.Name ] = newMod;
|
}
|
||||||
|
|
||||||
if( updateFileList && settings.Enabled )
|
AvailableMods[ data.BasePath.Name ] = new Mod.Mod( settings, data );
|
||||||
{
|
|
||||||
CalculateEffectiveFileList();
|
if( !updateFileList || !settings.Enabled )
|
||||||
if( data.Resources.MetaManipulations.Count > 0 )
|
{
|
||||||
{
|
return;
|
||||||
UpdateMetaManipulations();
|
}
|
||||||
}
|
|
||||||
}
|
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 ) )
|
if( !ResolvedFiles.TryGetValue( gameResourcePath, out var candidate ) )
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if( candidate.FullName.Length >= 260 || !candidate.Exists )
|
if( candidate.InternalName.Length > Utf8GamePath.MaxGamePathLength
|
||||||
|
|| candidate.IsRooted && !candidate.Exists )
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -396,9 +387,6 @@ public class ModCollectionCache
|
||||||
return candidate;
|
return candidate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public GamePath? GetSwappedFilePath( GamePath gameResourcePath )
|
public FullPath? ResolveSwappedOrReplacementPath( Utf8GamePath gameResourcePath )
|
||||||
=> SwappedFiles.TryGetValue( gameResourcePath, out var swappedPath ) ? swappedPath : null;
|
=> GetCandidateForGameFile( gameResourcePath );
|
||||||
|
|
||||||
public string? ResolveSwappedOrReplacementPath( GamePath gameResourcePath )
|
|
||||||
=> GetCandidateForGameFile( gameResourcePath )?.FullName.Replace( '\\', '/' ) ?? GetSwappedFilePath( gameResourcePath ) ?? null;
|
|
||||||
}
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Dalamud.Logging;
|
using Dalamud.Logging;
|
||||||
|
using Penumbra.GameData.ByteString;
|
||||||
using Penumbra.GameData.Util;
|
using Penumbra.GameData.Util;
|
||||||
using Penumbra.Meta;
|
using Penumbra.Meta;
|
||||||
using Penumbra.Mod;
|
using Penumbra.Mod;
|
||||||
|
|
@ -347,15 +348,7 @@ namespace Penumbra.Mods
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CheckCrc64( ulong crc )
|
public FullPath? ResolveSwappedOrReplacementPath( Utf8GamePath gameResourcePath )
|
||||||
{
|
|
||||||
if( Collections.ActiveCollection.Cache?.Checksums.Contains( crc ) ?? false )
|
|
||||||
return true;
|
|
||||||
|
|
||||||
return Collections.ForcedCollection.Cache?.Checksums.Contains( crc ) ?? false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string? ResolveSwappedOrReplacementPath( GamePath gameResourcePath )
|
|
||||||
{
|
{
|
||||||
var ret = Collections.ActiveCollection.ResolveSwappedOrReplacementPath( gameResourcePath );
|
var ret = Collections.ActiveCollection.ResolveSwappedOrReplacementPath( gameResourcePath );
|
||||||
ret ??= Collections.ForcedCollection.ResolveSwappedOrReplacementPath( gameResourcePath );
|
ret ??= Collections.ForcedCollection.ResolveSwappedOrReplacementPath( gameResourcePath );
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ using System.ComponentModel;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using Dalamud.Logging;
|
using Dalamud.Logging;
|
||||||
using Penumbra.Mod;
|
using Penumbra.Mod;
|
||||||
using Penumbra.Structs;
|
|
||||||
|
|
||||||
namespace Penumbra.Mods;
|
namespace Penumbra.Mods;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,22 +18,6 @@ using System.Linq;
|
||||||
|
|
||||||
namespace Penumbra;
|
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 class Penumbra : IDalamudPlugin
|
||||||
{
|
{
|
||||||
public string Name
|
public string Name
|
||||||
|
|
@ -54,6 +38,7 @@ public class Penumbra : IDalamudPlugin
|
||||||
|
|
||||||
|
|
||||||
public ResourceLoader ResourceLoader { get; }
|
public ResourceLoader ResourceLoader { get; }
|
||||||
|
public ResourceLogger ResourceLogger { get; }
|
||||||
|
|
||||||
//public PathResolver PathResolver { get; }
|
//public PathResolver PathResolver { get; }
|
||||||
public SettingsInterface SettingsInterface { get; }
|
public SettingsInterface SettingsInterface { get; }
|
||||||
|
|
@ -81,19 +66,18 @@ public class Penumbra : IDalamudPlugin
|
||||||
CharacterUtility = new CharacterUtility();
|
CharacterUtility = new CharacterUtility();
|
||||||
MetaDefaults = new MetaDefaults();
|
MetaDefaults = new MetaDefaults();
|
||||||
ResourceLoader = new ResourceLoader( this );
|
ResourceLoader = new ResourceLoader( this );
|
||||||
|
ResourceLogger = new ResourceLogger( ResourceLoader );
|
||||||
|
PlayerWatcher = PlayerWatchFactory.Create( Dalamud.Framework, Dalamud.ClientState, Dalamud.Objects );
|
||||||
ModManager = new ModManager();
|
ModManager = new ModManager();
|
||||||
ModManager.DiscoverMods();
|
ModManager.DiscoverMods();
|
||||||
//PathResolver = new PathResolver( ResourceLoader, gameUtils );
|
|
||||||
PlayerWatcher = PlayerWatchFactory.Create( Dalamud.Framework, Dalamud.ClientState, Dalamud.Objects );
|
|
||||||
ObjectReloader = new ObjectReloader( ModManager, Config.WaitFrames );
|
ObjectReloader = new ObjectReloader( ModManager, Config.WaitFrames );
|
||||||
|
//PathResolver = new PathResolver( ResourceLoader, gameUtils );
|
||||||
|
|
||||||
Dalamud.Commands.AddHandler( CommandName, new CommandInfo( OnCommand )
|
Dalamud.Commands.AddHandler( CommandName, new CommandInfo( OnCommand )
|
||||||
{
|
{
|
||||||
HelpMessage = "/penumbra - toggle ui\n/penumbra reload - reload mod file lists & discover any new mods",
|
HelpMessage = "/penumbra - toggle ui\n/penumbra reload - reload mod file lists & discover any new mods",
|
||||||
} );
|
} );
|
||||||
|
|
||||||
ResourceLoader.EnableReplacements();
|
|
||||||
ResourceLoader.EnableLogging();
|
|
||||||
if( Config.DebugMode )
|
if( Config.DebugMode )
|
||||||
{
|
{
|
||||||
ResourceLoader.EnableDebug();
|
ResourceLoader.EnableDebug();
|
||||||
|
|
@ -112,7 +96,7 @@ public class Penumbra : IDalamudPlugin
|
||||||
CreateWebServer();
|
CreateWebServer();
|
||||||
}
|
}
|
||||||
|
|
||||||
if( !Config.EnablePlayerWatch || !Config.IsEnabled )
|
if( !Config.EnablePlayerWatch || !Config.EnableMods )
|
||||||
{
|
{
|
||||||
PlayerWatcher.Disable();
|
PlayerWatcher.Disable();
|
||||||
}
|
}
|
||||||
|
|
@ -122,16 +106,25 @@ public class Penumbra : IDalamudPlugin
|
||||||
PluginLog.Debug( "Triggered Redraw of {Player}.", p.Name );
|
PluginLog.Debug( "Triggered Redraw of {Player}.", p.Name );
|
||||||
ObjectReloader.RedrawObject( p, RedrawType.OnlyWithSettings );
|
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()
|
public bool Enable()
|
||||||
{
|
{
|
||||||
if( Config.IsEnabled )
|
if( Config.EnableMods )
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Config.IsEnabled = true;
|
Config.EnableMods = true;
|
||||||
|
ResourceLoader.EnableReplacements();
|
||||||
ResidentResources.Reload();
|
ResidentResources.Reload();
|
||||||
if( Config.EnablePlayerWatch )
|
if( Config.EnablePlayerWatch )
|
||||||
{
|
{
|
||||||
|
|
@ -145,12 +138,13 @@ public class Penumbra : IDalamudPlugin
|
||||||
|
|
||||||
public bool Disable()
|
public bool Disable()
|
||||||
{
|
{
|
||||||
if( !Config.IsEnabled )
|
if( !Config.EnableMods )
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Config.IsEnabled = false;
|
Config.EnableMods = false;
|
||||||
|
ResourceLoader.DisableReplacements();
|
||||||
ResidentResources.Reload();
|
ResidentResources.Reload();
|
||||||
if( Config.EnablePlayerWatch )
|
if( Config.EnablePlayerWatch )
|
||||||
{
|
{
|
||||||
|
|
@ -219,7 +213,9 @@ public class Penumbra : IDalamudPlugin
|
||||||
Dalamud.Commands.RemoveHandler( CommandName );
|
Dalamud.Commands.RemoveHandler( CommandName );
|
||||||
|
|
||||||
//PathResolver.Dispose();
|
//PathResolver.Dispose();
|
||||||
|
ResourceLogger.Dispose();
|
||||||
ResourceLoader.Dispose();
|
ResourceLoader.Dispose();
|
||||||
|
|
||||||
|
|
||||||
ShutdownWebServer();
|
ShutdownWebServer();
|
||||||
}
|
}
|
||||||
|
|
@ -322,8 +318,8 @@ public class Penumbra : IDalamudPlugin
|
||||||
}
|
}
|
||||||
case "toggle":
|
case "toggle":
|
||||||
{
|
{
|
||||||
SetEnabled( !Config.IsEnabled );
|
SetEnabled( !Config.EnableMods );
|
||||||
Dalamud.Chat.Print( Config.IsEnabled
|
Dalamud.Chat.Print( Config.EnableMods
|
||||||
? modsEnabled
|
? modsEnabled
|
||||||
: modsDisabled );
|
: modsDisabled );
|
||||||
break;
|
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.Numerics;
|
||||||
using System.Security.Cryptography.X509Certificates;
|
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using ImGuiNET;
|
using ImGuiNET;
|
||||||
|
using Penumbra.GameData.ByteString;
|
||||||
|
|
||||||
namespace Penumbra.UI.Custom
|
namespace Penumbra.UI.Custom
|
||||||
{
|
{
|
||||||
|
|
@ -20,6 +20,19 @@ namespace Penumbra.UI.Custom
|
||||||
ImGui.SetTooltip( "Click to copy to clipboard." );
|
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
|
public static partial class ImGuiCustom
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,21 @@ public partial class SettingsInterface
|
||||||
private string _filePathFilter = string.Empty;
|
private string _filePathFilter = string.Empty;
|
||||||
private string _filePathFilterLower = string.Empty;
|
private string _filePathFilterLower = string.Empty;
|
||||||
|
|
||||||
private readonly float _leftTextLength =
|
private const float LeftTextLength = 600;
|
||||||
ImGui.CalcTextSize( "chara/human/c0000/obj/body/b0000/material/v0000/mt_c0000b0000_b.mtrl" ).X / ImGuiHelpers.GlobalScale + 40;
|
|
||||||
|
|
||||||
private float _arrowLength = 0;
|
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 )
|
private static void DrawLine( string path, string name )
|
||||||
{
|
{
|
||||||
ImGui.TableNextColumn();
|
ImGui.TableNextColumn();
|
||||||
|
|
@ -45,13 +55,13 @@ public partial class SettingsInterface
|
||||||
_arrowLength = ImGui.CalcTextSize( FontAwesomeIcon.LongArrowAltLeft.ToIconString() ).X / ImGuiHelpers.GlobalScale;
|
_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 ) )
|
if( ImGui.InputTextWithHint( "##effective_changes_gfilter", "Filter game path...", ref _gamePathFilter, 256 ) )
|
||||||
{
|
{
|
||||||
_gamePathFilterLower = _gamePathFilter.ToLowerInvariant();
|
_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 );
|
ImGui.SetNextItemWidth( -1 );
|
||||||
if( ImGui.InputTextWithHint( "##effective_changes_ffilter", "Filter file path...", ref _filePathFilter, 256 ) )
|
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 ) )
|
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 );
|
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 ) )
|
if( _gamePathFilter.Any() && !kvp.Key.ToString().Contains( _gamePathFilterLower ) )
|
||||||
{
|
{
|
||||||
|
|
@ -94,11 +104,6 @@ public partial class SettingsInterface
|
||||||
void DrawFileLines( ModCollectionCache cache )
|
void DrawFileLines( ModCollectionCache cache )
|
||||||
{
|
{
|
||||||
foreach( var (gp, fp) in cache.ResolvedFiles.Where( CheckFilters ) )
|
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 );
|
DrawLine( gp, fp );
|
||||||
}
|
}
|
||||||
|
|
@ -139,75 +144,67 @@ public partial class SettingsInterface
|
||||||
var activeCollection = modManager.Collections.ActiveCollection.Cache;
|
var activeCollection = modManager.Collections.ActiveCollection.Cache;
|
||||||
var forcedCollection = modManager.Collections.ForcedCollection.Cache;
|
var forcedCollection = modManager.Collections.ForcedCollection.Cache;
|
||||||
|
|
||||||
var (activeResolved, activeSwap, activeMeta) = activeCollection != null
|
var (activeResolved, activeMeta) = activeCollection != null
|
||||||
? ( activeCollection.ResolvedFiles.Count, activeCollection.SwappedFiles.Count, activeCollection.MetaManipulations.Count )
|
? ( activeCollection.ResolvedFiles.Count, activeCollection.MetaManipulations.Count )
|
||||||
: ( 0, 0, 0 );
|
: ( 0, 0 );
|
||||||
var (forcedResolved, forcedSwap, forcedMeta) = forcedCollection != null
|
var (forcedResolved, forcedMeta) = forcedCollection != null
|
||||||
? ( forcedCollection.ResolvedFiles.Count, forcedCollection.SwappedFiles.Count, forcedCollection.MetaManipulations.Count )
|
? ( forcedCollection.ResolvedFiles.Count, forcedCollection.MetaManipulations.Count )
|
||||||
: ( 0, 0, 0 );
|
: ( 0, 0 );
|
||||||
var totalLines = activeResolved + forcedResolved + activeSwap + forcedSwap + activeMeta + forcedMeta;
|
var totalLines = activeResolved + forcedResolved + activeMeta + forcedMeta;
|
||||||
if( totalLines == 0 )
|
if( totalLines == 0 )
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if( ImGui.BeginTable( "##effective_changes", 2, flags, AutoFillSize ) )
|
if( !ImGui.BeginTable( "##effective_changes", 2, flags, AutoFillSize ) )
|
||||||
{
|
{
|
||||||
raii.Push( ImGui.EndTable );
|
return;
|
||||||
ImGui.TableSetupColumn( "##tableGamePathCol", ImGuiTableColumnFlags.None, _leftTextLength * ImGuiHelpers.GlobalScale );
|
}
|
||||||
|
|
||||||
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;
|
for( var actualRow = clipper.DisplayStart; actualRow < clipper.DisplayEnd; actualRow++ )
|
||||||
unsafe
|
|
||||||
{
|
{
|
||||||
clipper = new ImGuiListClipperPtr( ImGuiNative.ImGuiListClipper_ImGuiListClipper() );
|
var row = actualRow;
|
||||||
}
|
ImGui.TableNextRow();
|
||||||
|
if( row < activeResolved )
|
||||||
clipper.Begin( totalLines );
|
|
||||||
|
|
||||||
|
|
||||||
while( clipper.Step() )
|
|
||||||
{
|
|
||||||
for( var actualRow = clipper.DisplayStart; actualRow < clipper.DisplayEnd; actualRow++ )
|
|
||||||
{
|
{
|
||||||
var row = actualRow;
|
var (gamePath, file) = activeCollection!.ResolvedFiles.ElementAt( row );
|
||||||
ImGui.TableNextRow();
|
DrawLine( gamePath, file );
|
||||||
if( row < activeResolved )
|
}
|
||||||
{
|
else if( ( row -= activeResolved ) < activeMeta )
|
||||||
var (gamePath, file) = activeCollection!.ResolvedFiles.ElementAt( row );
|
{
|
||||||
DrawLine( gamePath, file.FullName );
|
var (manip, mod) = activeCollection!.MetaManipulations.Manipulations.ElementAt( row );
|
||||||
}
|
DrawLine( manip.IdentifierString(), mod.Data.Meta.Name );
|
||||||
else if( ( row -= activeResolved ) < activeSwap )
|
}
|
||||||
{
|
else if( ( row -= activeMeta ) < forcedResolved )
|
||||||
var (gamePath, swap) = activeCollection!.SwappedFiles.ElementAt( row );
|
{
|
||||||
DrawLine( gamePath, swap );
|
var (gamePath, file) = forcedCollection!.ResolvedFiles.ElementAt( row );
|
||||||
}
|
DrawLine( gamePath, file );
|
||||||
else if( ( row -= activeSwap ) < activeMeta )
|
}
|
||||||
{
|
else
|
||||||
var (manip, mod) = activeCollection!.MetaManipulations.Manipulations.ElementAt( row );
|
{
|
||||||
DrawLine( manip.IdentifierString(), mod.Data.Meta.Name );
|
row -= forcedResolved;
|
||||||
}
|
var (manip, mod) = forcedCollection!.MetaManipulations.Manipulations.ElementAt( row );
|
||||||
else if( ( row -= activeMeta ) < forcedResolved )
|
DrawLine( manip.IdentifierString(), mod.Data.Meta.Name );
|
||||||
{
|
|
||||||
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 );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ using Penumbra.GameData.Util;
|
||||||
using Penumbra.Meta;
|
using Penumbra.Meta;
|
||||||
using Penumbra.Mod;
|
using Penumbra.Mod;
|
||||||
using Penumbra.Mods;
|
using Penumbra.Mods;
|
||||||
using Penumbra.Structs;
|
|
||||||
using Penumbra.UI.Custom;
|
using Penumbra.UI.Custom;
|
||||||
using Penumbra.Util;
|
using Penumbra.Util;
|
||||||
using ImGui = ImGuiNET.ImGui;
|
using ImGui = ImGuiNET.ImGui;
|
||||||
|
|
@ -56,7 +55,7 @@ public partial class SettingsInterface
|
||||||
private Option? _selectedOption;
|
private Option? _selectedOption;
|
||||||
private string _currentGamePaths = "";
|
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 Selector _selector;
|
||||||
private readonly SettingsInterface _base;
|
private readonly SettingsInterface _base;
|
||||||
|
|
@ -218,7 +217,10 @@ public partial class SettingsInterface
|
||||||
indent.Push( 15f );
|
indent.Push( 15f );
|
||||||
foreach( var file in files )
|
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 )
|
foreach( var manip in manipulations )
|
||||||
|
|
@ -258,13 +260,13 @@ public partial class SettingsInterface
|
||||||
foreach( var (source, target) in Meta.FileSwaps )
|
foreach( var (source, target) in Meta.FileSwaps )
|
||||||
{
|
{
|
||||||
ImGui.TableNextColumn();
|
ImGui.TableNextColumn();
|
||||||
ImGuiCustom.CopyOnClickSelectable( source );
|
ImGuiCustom.CopyOnClickSelectable( source.Path );
|
||||||
|
|
||||||
ImGui.TableNextColumn();
|
ImGui.TableNextColumn();
|
||||||
ImGuiCustom.PrintIcon( FontAwesomeIcon.LongArrowAltRight );
|
ImGuiCustom.PrintIcon( FontAwesomeIcon.LongArrowAltRight );
|
||||||
|
|
||||||
ImGui.TableNextColumn();
|
ImGui.TableNextColumn();
|
||||||
ImGuiCustom.CopyOnClickSelectable( target );
|
ImGuiCustom.CopyOnClickSelectable( target.InternalName );
|
||||||
|
|
||||||
ImGui.TableNextRow();
|
ImGui.TableNextRow();
|
||||||
}
|
}
|
||||||
|
|
@ -278,7 +280,8 @@ public partial class SettingsInterface
|
||||||
}
|
}
|
||||||
|
|
||||||
_fullFilenameList = Mod.Data.Resources.ModFiles
|
_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 )
|
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;
|
removeFolders = 0;
|
||||||
var defaultIndex =
|
var defaultIndex = gamePaths.IndexOf( p => p.Path.StartsWith( DefaultUtf8GamePath ) );
|
||||||
gamePaths.IndexOf( p => ( ( string )p ).StartsWith( TextDefaultGamePath ) );
|
|
||||||
if( defaultIndex < 0 )
|
if( defaultIndex < 0 )
|
||||||
{
|
{
|
||||||
return defaultIndex;
|
return defaultIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
string path = gamePaths[ defaultIndex ];
|
var path = gamePaths[ defaultIndex ].Path;
|
||||||
if( path.Length == TextDefaultGamePath.Length )
|
if( path.Length == TextDefaultGamePath.Length )
|
||||||
{
|
{
|
||||||
return defaultIndex;
|
return defaultIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
if( path[ TextDefaultGamePath.Length ] != '-'
|
if( path[ TextDefaultGamePath.Length ] != ( byte )'-'
|
||||||
|| !int.TryParse( path.Substring( TextDefaultGamePath.Length + 1 ), out removeFolders ) )
|
|| !int.TryParse( path.Substring( TextDefaultGamePath.Length + 1 ).ToString(), out removeFolders ) )
|
||||||
{
|
{
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
@ -373,8 +375,9 @@ public partial class SettingsInterface
|
||||||
|
|
||||||
var option = ( Option )_selectedOption;
|
var option = ( Option )_selectedOption;
|
||||||
|
|
||||||
var gamePaths = _currentGamePaths.Split( ';' ).Select( p => new GamePath( p ) ).ToArray();
|
var gamePaths = _currentGamePaths.Split( ';' )
|
||||||
if( gamePaths.Length == 0 || ( ( string )gamePaths[ 0 ] ).Length == 0 )
|
.Select( p => Utf8GamePath.FromString( p, out var path, false ) ? path : Utf8GamePath.Empty ).Where( p => !p.IsEmpty ).ToArray();
|
||||||
|
if( gamePaths.Length == 0 )
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -517,18 +520,18 @@ public partial class SettingsInterface
|
||||||
{
|
{
|
||||||
Selectable( 0, ColorGreen );
|
Selectable( 0, ColorGreen );
|
||||||
|
|
||||||
using var indent = ImGuiRaii.PushIndent( indentWidth );
|
using var indent = ImGuiRaii.PushIndent( indentWidth );
|
||||||
var tmpPaths = gamePaths.ToArray();
|
foreach( var gamePath in gamePaths.ToArray() )
|
||||||
foreach( var gamePath in tmpPaths )
|
|
||||||
{
|
{
|
||||||
string tmp = gamePath;
|
var tmp = gamePath.ToString();
|
||||||
|
var old = tmp;
|
||||||
if( ImGui.InputText( $"##{fileName}_{gamePath}", ref tmp, 128, ImGuiInputTextFlags.EnterReturnsTrue )
|
if( ImGui.InputText( $"##{fileName}_{gamePath}", ref tmp, 128, ImGuiInputTextFlags.EnterReturnsTrue )
|
||||||
&& tmp != gamePath )
|
&& tmp != old )
|
||||||
{
|
{
|
||||||
gamePaths.Remove( gamePath );
|
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 )
|
else if( gamePaths.Count == 0 )
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -3,136 +3,179 @@ using System.Linq;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using ImGuiNET;
|
using ImGuiNET;
|
||||||
|
using Penumbra.GameData.ByteString;
|
||||||
using Penumbra.GameData.Util;
|
using Penumbra.GameData.Util;
|
||||||
|
using Penumbra.Mod;
|
||||||
using Penumbra.Mods;
|
using Penumbra.Mods;
|
||||||
using Penumbra.Structs;
|
|
||||||
using Penumbra.UI.Custom;
|
using Penumbra.UI.Custom;
|
||||||
using Penumbra.Util;
|
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";
|
ImGui.SetNextItemWidth( OptionSelectionWidth * ImGuiHelpers.GlobalScale );
|
||||||
private const string LabelNewSingleGroupEdit = "##newSingleGroup";
|
if( Meta!.Groups.Count == 0 )
|
||||||
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 );
|
ImGui.Combo( LabelGroupSelect, ref _selectedGroupIndex, TextNoOptionAvailable, 1 );
|
||||||
if( Meta!.Groups.Count == 0 )
|
return false;
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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.SameLine();
|
||||||
ImGui.SetNextItemWidth( OptionSelectionWidth );
|
var newName = opt.OptionName;
|
||||||
if( ( _selectedGroup?.Options.Count ?? 0 ) == 0 )
|
|
||||||
|
if( nameBoxStart == CheckMarkSize )
|
||||||
{
|
{
|
||||||
ImGui.Combo( LabelOptionSelect, ref _selectedOptionIndex, TextNoOptionAvailable, 1 );
|
nameBoxStart = ImGui.GetCursorPosX();
|
||||||
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 );
|
ImGui.SetNextItemWidth( MultiEditBoxWidth * ImGuiHelpers.GlobalScale );
|
||||||
if( ImGui.InputTextWithHint( $"##new_{group.GroupName}_l", "Add new option...", ref newOption, 64,
|
if( ImGui.InputText( $"{label}_l", ref newName, 64, ImGuiInputTextFlags.EnterReturnsTrue ) )
|
||||||
ImGuiInputTextFlags.EnterReturnsTrue )
|
|
||||||
&& newOption.Length != 0 )
|
|
||||||
{
|
{
|
||||||
group.Options.Add( new Option()
|
if( newName.Length == 0 )
|
||||||
{ OptionName = newOption, OptionDesc = "", OptionFiles = new Dictionary< RelPath, HashSet< GamePath > >() } );
|
{
|
||||||
_selector.SaveCurrentMod();
|
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() )
|
if( Mod!.Data.Meta.RefreshHasGroupsWithConfig() )
|
||||||
{
|
{
|
||||||
_selector.Cache.TriggerFilterReset();
|
_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;
|
if( Penumbra.ModManager.ChangeModGroup( group.GroupName, groupName, Mod.Data ) && Mod.Data.Meta.RefreshHasGroupsWithConfig() )
|
||||||
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 ];
|
_selector.Cache.TriggerFilterReset();
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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 ];
|
if( code == group.Options.Count )
|
||||||
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( newName.Length > 0 )
|
||||||
{
|
{
|
||||||
if( newName.Length > 0 )
|
Mod.Settings.Settings[ group.GroupName ] = code;
|
||||||
|
group.Options.Add( new Option()
|
||||||
{
|
{
|
||||||
Mod.Settings.Settings[ group.GroupName ] = code;
|
OptionName = newName,
|
||||||
group.Options.Add( new Option()
|
OptionDesc = "",
|
||||||
{
|
OptionFiles = new Dictionary< Utf8RelPath, HashSet< Utf8GamePath > >(),
|
||||||
OptionName = newName,
|
} );
|
||||||
OptionDesc = "",
|
_selector.SaveCurrentMod();
|
||||||
OptionFiles = new Dictionary< RelPath, HashSet< GamePath > >(),
|
}
|
||||||
} );
|
}
|
||||||
_selector.SaveCurrentMod();
|
else
|
||||||
}
|
{
|
||||||
|
if( newName.Length == 0 )
|
||||||
|
{
|
||||||
|
Penumbra.ModManager.RemoveModOption( code, group, Mod.Data );
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if( newName.Length == 0 )
|
if( newName != group.Options[ code ].OptionName )
|
||||||
{
|
{
|
||||||
Penumbra.ModManager.RemoveModOption( code, group, Mod.Data );
|
group.Options[ code ] = new Option()
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if( newName != group.Options[ code ].OptionName )
|
|
||||||
{
|
{
|
||||||
group.Options[ code ] = new Option()
|
OptionName = newName, OptionDesc = group.Options[ code ].OptionDesc,
|
||||||
{
|
OptionFiles = group.Options[ code ].OptionFiles,
|
||||||
OptionName = newName, OptionDesc = group.Options[ code ].OptionDesc,
|
};
|
||||||
OptionFiles = group.Options[ code ].OptionFiles,
|
_selector.SaveCurrentMod();
|
||||||
};
|
|
||||||
_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();
|
ImGui.SameLine();
|
||||||
var labelEditPos = ImGui.GetCursorPosX();
|
ImGuiCustom.PrintIcon( FontAwesomeIcon.LongArrowAltRight );
|
||||||
DrawSingleSelectorEditGroup( group );
|
ImGui.SameLine();
|
||||||
|
|
||||||
return labelEditPos;
|
ImGui.SetNextItemWidth( width );
|
||||||
}
|
if( ImGui.InputTextWithHint( $"##swapRhs_{idx}", "Enter new replacement path...", ref valueString,
|
||||||
|
GamePath.MaxGamePathLength,
|
||||||
private void DrawAddSingleGroupField( float labelEditPos )
|
ImGuiInputTextFlags.EnterReturnsTrue ) )
|
||||||
{
|
|
||||||
var newGroup = "";
|
|
||||||
ImGui.SetCursorPosX( labelEditPos );
|
|
||||||
if( labelEditPos == CheckMarkSize )
|
|
||||||
{
|
{
|
||||||
ImGui.SetNextItemWidth( MultiEditBoxWidth * ImGuiHelpers.GlobalScale );
|
var newValue = new FullPath( valueString.ToLowerInvariant() );
|
||||||
}
|
if( newValue.CompareTo( value ) != 0 )
|
||||||
|
|
||||||
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 newKey = new GamePath( keyString );
|
Meta.FileSwaps[ key ] = newValue;
|
||||||
if( newKey.CompareTo( key ) != 0 )
|
_selector.SaveCurrentMod();
|
||||||
{
|
_selector.Cache.TriggerListReset();
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -488,7 +488,7 @@ public partial class SettingsInterface
|
||||||
|
|
||||||
using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup );
|
using var raii = ImGuiRaii.DeferredEnd( ImGui.EndPopup );
|
||||||
|
|
||||||
if( ModPanel.DrawSortOrder( mod.Data, _modManager, this ) )
|
if( ModPanel.DrawSortOrder( mod.Data, Penumbra.ModManager, this ) )
|
||||||
{
|
{
|
||||||
ImGui.CloseCurrentPopup();
|
ImGui.CloseCurrentPopup();
|
||||||
}
|
}
|
||||||
|
|
@ -509,7 +509,7 @@ public partial class SettingsInterface
|
||||||
{
|
{
|
||||||
var change = false;
|
var change = false;
|
||||||
var metaManips = 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++ );
|
var (mod, _, _) = Cache.GetMod( currentIdx++ );
|
||||||
if( mod != null )
|
if( mod != null )
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,10 @@ using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using Dalamud.Interface.Components;
|
using Dalamud.Interface.Components;
|
||||||
|
using Dalamud.Logging;
|
||||||
using ImGuiNET;
|
using ImGuiNET;
|
||||||
using Penumbra.GameData.ByteString;
|
using Penumbra.GameData.ByteString;
|
||||||
using Penumbra.Interop;
|
using Penumbra.Interop;
|
||||||
|
|
@ -131,7 +133,7 @@ public partial class SettingsInterface
|
||||||
|
|
||||||
private void DrawEnabledBox()
|
private void DrawEnabledBox()
|
||||||
{
|
{
|
||||||
var enabled = _config.IsEnabled;
|
var enabled = _config.EnableMods;
|
||||||
if( ImGui.Checkbox( "Enable Mods", ref enabled ) )
|
if( ImGui.Checkbox( "Enable Mods", ref enabled ) )
|
||||||
{
|
{
|
||||||
_base._penumbra.SetEnabled( enabled );
|
_base._penumbra.SetEnabled( enabled );
|
||||||
|
|
@ -317,14 +319,84 @@ public partial class SettingsInterface
|
||||||
+ "You usually should not need to do this." );
|
+ "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()
|
private void DrawAdvancedSettings()
|
||||||
{
|
{
|
||||||
DrawTempFolder();
|
DrawTempFolder();
|
||||||
|
DrawRequestedResourceLogging();
|
||||||
DrawDisableSoundStreamingBox();
|
DrawDisableSoundStreamingBox();
|
||||||
DrawLogLoadedFilesBox();
|
DrawLogLoadedFilesBox();
|
||||||
DrawDisableNotificationsBox();
|
DrawDisableNotificationsBox();
|
||||||
DrawEnableHttpApiBox();
|
DrawEnableHttpApiBox();
|
||||||
DrawReloadResourceButton();
|
DrawReloadResourceButton();
|
||||||
|
DrawEnableDebugModeBox();
|
||||||
|
DrawEnableFullResourceLoggingBox();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static unsafe void Text( Utf8String s )
|
public static unsafe void Text( Utf8String s )
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using ImGuiNET;
|
using ImGuiNET;
|
||||||
using Penumbra.Mods;
|
|
||||||
using Penumbra.UI.Custom;
|
using Penumbra.UI.Custom;
|
||||||
using Penumbra.Util;
|
|
||||||
|
|
||||||
namespace Penumbra.UI;
|
namespace Penumbra.UI;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,36 +5,35 @@ using Lumina.Excel.GeneratedSheets;
|
||||||
using Penumbra.GameData.Enums;
|
using Penumbra.GameData.Enums;
|
||||||
using Penumbra.UI.Custom;
|
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;
|
_penumbra.Api.InvokeClick( ret, data );
|
||||||
ret = ImGui.IsItemClicked( ImGuiMouseButton.Right ) ? MouseButton.Right : ret;
|
}
|
||||||
ret = ImGui.IsItemClicked( ImGuiMouseButton.Middle ) ? MouseButton.Middle : ret;
|
|
||||||
|
|
||||||
if( ret != MouseButton.None )
|
if( _penumbra.Api.HasTooltip && ImGui.IsItemHovered() )
|
||||||
{
|
{
|
||||||
_penumbra.Api.InvokeClick( ret, data );
|
ImGui.BeginTooltip();
|
||||||
}
|
using var tooltip = ImGuiRaii.DeferredEnd( ImGui.EndTooltip );
|
||||||
|
_penumbra.Api.InvokeTooltip( data );
|
||||||
|
}
|
||||||
|
|
||||||
if( _penumbra.Api.HasTooltip && ImGui.IsItemHovered() )
|
if( data is Item it )
|
||||||
{
|
{
|
||||||
ImGui.BeginTooltip();
|
var modelId = $"({( ( Quad )it.ModelMain ).A})";
|
||||||
using var tooltip = ImGuiRaii.DeferredEnd( ImGui.EndTooltip );
|
var offset = ImGui.CalcTextSize( modelId ).X - ImGui.GetStyle().ItemInnerSpacing.X + itemIdOffset;
|
||||||
_penumbra.Api.InvokeTooltip( data );
|
|
||||||
}
|
|
||||||
|
|
||||||
if( data is Item it )
|
ImGui.SameLine( ImGui.GetWindowContentRegionWidth() - offset );
|
||||||
{
|
ImGui.TextColored( new Vector4( 0.5f, 0.5f, 0.5f, 1 ), modelId );
|
||||||
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 );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,87 +1,33 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
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];
|
if( match( array[ i ] ) )
|
||||||
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 ] ) )
|
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( predicate.Invoke( array[ i ] ) )
|
||||||
if( idx1 < 0 )
|
|
||||||
{
|
{
|
||||||
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 )
|
return -1;
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3,135 +3,54 @@ using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Runtime.InteropServices;
|
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>
|
ReadOnlySpan< byte > data = br.ReadBytes( Unsafe.SizeOf< T >() );
|
||||||
/// 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 >() );
|
|
||||||
|
|
||||||
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>
|
return list;
|
||||||
/// 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 );
|
/// <summary>
|
||||||
|
/// Seeks this BinaryReader's position to the given offset. Syntactic sugar.
|
||||||
for( var i = 0; i < count; i++ )
|
/// </summary>
|
||||||
{
|
public static void Seek( this BinaryReader br, long offset )
|
||||||
var offset = size * i;
|
{
|
||||||
var span = new ReadOnlySpan< byte >( data, offset, size );
|
br.BaseStream.Position = offset;
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2,36 +2,34 @@ using System.Collections.Generic;
|
||||||
using Dalamud.Game.Text;
|
using Dalamud.Game.Text;
|
||||||
using Dalamud.Game.Text.SeStringHandling;
|
using Dalamud.Game.Text.SeStringHandling;
|
||||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||||
using Dalamud.Plugin;
|
|
||||||
using Lumina.Excel.GeneratedSheets;
|
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 UIForegroundPayload( ( ushort )( 0x223 + item.Rarity * 2 ) ),
|
new ItemPayload( item.RowId, false ),
|
||||||
new UIGlowPayload( ( ushort )( 0x224 + item.Rarity * 2 ) ),
|
new UIForegroundPayload( 500 ),
|
||||||
new ItemPayload( item.RowId, false ),
|
new UIGlowPayload( 501 ),
|
||||||
new UIForegroundPayload( 500 ),
|
new TextPayload( $"{( char )SeIconChar.LinkMarker}" ),
|
||||||
new UIGlowPayload( 501 ),
|
new UIForegroundPayload( 0 ),
|
||||||
new TextPayload( $"{( char )SeIconChar.LinkMarker}" ),
|
new UIGlowPayload( 0 ),
|
||||||
new UIForegroundPayload( 0 ),
|
new TextPayload( item.Name ),
|
||||||
new UIGlowPayload( 0 ),
|
new RawPayload( new byte[] { 0x02, 0x27, 0x07, 0xCF, 0x01, 0x01, 0x01, 0xFF, 0x01, 0x03 } ),
|
||||||
new TextPayload( item.Name ),
|
new RawPayload( new byte[] { 0x02, 0x13, 0x02, 0xEC, 0x03 } ),
|
||||||
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
|
Dalamud.Chat.PrintChat( new XivChatEntry
|
||||||
{
|
{
|
||||||
Message = payload,
|
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.Threading.Tasks;
|
||||||
using System.Windows.Forms;
|
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();
|
_form = form;
|
||||||
return form.ShowDialogAsync( new DialogHandle( process.MainWindowHandle ) );
|
_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 >();
|
Hide();
|
||||||
var th = new Thread( () => DialogThread( form, owner, taskSource ) );
|
try
|
||||||
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 )
|
|
||||||
{
|
{
|
||||||
_form = form;
|
var result = _form.ShowDialog( _owner );
|
||||||
_owner = owner;
|
_taskSource.SetResult( result );
|
||||||
_taskSource = taskSource;
|
}
|
||||||
|
catch( Exception e )
|
||||||
Opacity = 0;
|
{
|
||||||
FormBorderStyle = FormBorderStyle.None;
|
_taskSource.SetException( e );
|
||||||
ShowInTaskbar = false;
|
|
||||||
Size = new Size( 0, 0 );
|
|
||||||
|
|
||||||
Shown += HiddenForm_Shown;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HiddenForm_Shown( object? sender, EventArgs _ )
|
Close();
|
||||||
{
|
|
||||||
Hide();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var result = _form.ShowDialog( _owner );
|
|
||||||
_taskSource.SetResult( result );
|
|
||||||
}
|
|
||||||
catch( Exception e )
|
|
||||||
{
|
|
||||||
_taskSource.SetException( e );
|
|
||||||
}
|
|
||||||
|
|
||||||
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 const string MaterialFormat = "/mt_c0201b0001_{0}.mtrl";
|
||||||
public static readonly Regex MaterialRegex = new(@"/mt_c0201b0001_.*?\.mtrl", RegexOptions.Compiled);
|
public static readonly Regex MaterialRegex = new(@"/mt_c0201b0001_.*?\.mtrl", RegexOptions.Compiled);
|
||||||
|
|
||||||
|
|
||||||
public static bool ValidStrings( string from, string to )
|
public static bool ValidStrings( string from, string to )
|
||||||
=> from.Length != 0
|
=> from.Length != 0
|
||||||
&& to.Length != 0
|
&& to.Length != 0
|
||||||
|
|
@ -40,8 +39,8 @@ public static class ModelChanger
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var data = File.ReadAllBytes( file.FullName );
|
var data = File.ReadAllBytes( file.FullName );
|
||||||
var mdlFile = new MdlFile( data );
|
var mdlFile = new MdlFile( data );
|
||||||
Func< string, bool > compare = MaterialRegex.IsMatch;
|
Func< string, bool > compare = MaterialRegex.IsMatch;
|
||||||
if( from.Length > 0 )
|
if( from.Length > 0 )
|
||||||
{
|
{
|
||||||
|
|
@ -53,9 +52,9 @@ public static class ModelChanger
|
||||||
var replaced = 0;
|
var replaced = 0;
|
||||||
for( var i = 0; i < mdlFile.Materials.Length; ++i )
|
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;
|
++replaced;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,432 +3,405 @@ using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
|
||||||
using Lumina;
|
|
||||||
using Lumina.Data.Structs;
|
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 )
|
return Reader.ReadStructure< SqPackHeader >();
|
||||||
: this( file.OpenRead() )
|
}
|
||||||
{ }
|
|
||||||
|
|
||||||
public PenumbraSqPackStream( Stream stream )
|
public SqPackFileInfo GetFileMetadata( long offset )
|
||||||
{
|
{
|
||||||
BaseStream = stream;
|
BaseStream.Position = offset;
|
||||||
Reader = new BinaryReader( BaseStream );
|
|
||||||
}
|
|
||||||
|
|
||||||
public SqPackHeader GetSqPackHeader()
|
return Reader.ReadStructure< SqPackFileInfo >();
|
||||||
{
|
}
|
||||||
BaseStream.Position = 0;
|
|
||||||
|
|
||||||
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;
|
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 >();
|
case FileType.Model:
|
||||||
var file = Activator.CreateInstance< T >();
|
ReadModelFile( file, ms );
|
||||||
|
break;
|
||||||
|
|
||||||
// check if we need to read the extended model header or just default to the standard file header
|
case FileType.Texture:
|
||||||
if( fileInfo.Type == FileType.Model )
|
ReadTextureFile( file, ms );
|
||||||
{
|
break;
|
||||||
BaseStream.Position = offset;
|
|
||||||
|
|
||||||
var modelFileInfo = Reader.ReadStructure< ModelBlock >();
|
default: throw new NotImplementedException( $"File Type {( uint )fileInfo.Type} is not implemented." );
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 );
|
Debug.WriteLine( "Read data size does not match file size." );
|
||||||
|
|
||||||
foreach( var block in blocks )
|
|
||||||
{
|
|
||||||
ReadFileBlock( resource.FileInfo.Offset + resource.FileInfo.HeaderSize + block.Offset, ms );
|
|
||||||
}
|
|
||||||
|
|
||||||
// reset position ready for reading
|
|
||||||
ms.Position = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
ReadFileBlock( resource.FileInfo.Offset + resource.FileInfo.HeaderSize + block.Offset, ms );
|
||||||
var baseOffset = resource.FileInfo.Offset + resource.FileInfo.HeaderSize;
|
}
|
||||||
|
|
||||||
// 1/1/3/3/3 stack/runtime/vertex/egeo/index
|
// reset position ready for reading
|
||||||
// TODO: consider testing if this is more reliable than the Explorer method
|
ms.Position = 0;
|
||||||
// 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...
|
private unsafe void ReadModelFile( PenumbraFileResource resource, MemoryStream ms )
|
||||||
int totalBlocks = mdlBlock.StackBlockNum;
|
{
|
||||||
totalBlocks += mdlBlock.RuntimeBlockNum;
|
var mdlBlock = resource.FileInfo!.ModelBlock;
|
||||||
for( var i = 0; i < 3; i++ )
|
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 ];
|
var currentVertexOffset = ( int )ms.Position;
|
||||||
}
|
if( i == 0 || currentVertexOffset != vertexDataOffsets[ i - 1 ] )
|
||||||
|
|
||||||
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;
|
vertexDataOffsets[ i ] = currentVertexOffset;
|
||||||
if( i == 0 || currentVertexOffset != vertexDataOffsets[ i - 1 ] )
|
}
|
||||||
{
|
else
|
||||||
vertexDataOffsets[ i ] = currentVertexOffset;
|
{
|
||||||
}
|
vertexDataOffsets[ i ] = 0;
|
||||||
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++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
{
|
vertexBufferSizes[ i ] += ( int )ReadFileBlock( ms );
|
||||||
var lastPos = Reader.BaseStream.Position;
|
Reader.Seek( lastPos + compressedBlockSizes[ currentBlock ] );
|
||||||
ReadFileBlock( ms );
|
currentBlock++;
|
||||||
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++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ms.Seek( 0, SeekOrigin.Begin );
|
if( mdlBlock.EdgeGeometryVertexBufferBlockNum[ i ] != 0 )
|
||||||
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++ )
|
|
||||||
{
|
{
|
||||||
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++ )
|
// i guess this is only needed in the vertex area, for i = 0
|
||||||
{
|
// Reader.Seek( baseOffset + mdlBlock.IndexBufferOffset[ i ] );
|
||||||
ms.Write( BitConverter.GetBytes( vertexBufferSizes[ i ] ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
for( var i = 0; i < 3; i++ )
|
for( var j = 0; j < mdlBlock.IndexBufferBlockNum[ i ]; j++ )
|
||||||
{
|
{
|
||||||
ms.Write( BitConverter.GetBytes( indexBufferSizes[ i ] ) );
|
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
|
for( var i = 0; i < 3; i++ )
|
||||||
// will not be 0
|
{
|
||||||
var mipMapSize = blocks[ 0 ].CompressedOffset;
|
ms.Write( BitConverter.GetBytes( indexDataOffsets[ i ] ) );
|
||||||
if( mipMapSize != 0 )
|
}
|
||||||
|
|
||||||
|
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;
|
runningBlockTotal += ( uint )Reader.ReadInt16();
|
||||||
|
|
||||||
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 );
|
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 )
|
protected uint ReadFileBlock( MemoryStream dest, bool resetPosition = false )
|
||||||
=> ReadFileBlock( Reader.BaseStream.Position, dest, resetPosition );
|
=> 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;
|
dest.Write( Reader.ReadBytes( ( int )blockHeader.UncompressedSize ) );
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 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;
|
BaseStream.Position = originalPosition;
|
||||||
public FileType Type;
|
|
||||||
public uint RawFileSize;
|
|
||||||
public uint BlockCount;
|
|
||||||
|
|
||||||
public long Offset { get; internal set; }
|
|
||||||
|
|
||||||
public ModelBlock ModelBlock { get; internal set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
// this function is intentionally left blank
|
||||||
{ }
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[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;
|
||||||
using Newtonsoft.Json.Linq;
|
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 )
|
var token = JToken.Load( reader );
|
||||||
=> objectType == typeof( HashSet< T > );
|
|
||||||
|
|
||||||
public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer )
|
if( token.Type == JTokenType.Array )
|
||||||
{
|
{
|
||||||
var token = JToken.Load( reader );
|
return token.ToObject< HashSet< T > >() ?? new HashSet< T >();
|
||||||
|
|
||||||
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 >();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool CanWrite
|
var tmp = token.ToObject< T >();
|
||||||
=> true;
|
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();
|
var v = ( HashSet< T > )value;
|
||||||
if( value != null )
|
foreach( var val in v )
|
||||||
{
|
{
|
||||||
var v = ( HashSet< T > )value;
|
serializer.Serialize( writer, val?.ToString() );
|
||||||
foreach( var val in v )
|
|
||||||
{
|
|
||||||
serializer.Serialize( writer, val?.ToString() );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
writer.WriteEndArray();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
writer.WriteEndArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2,67 +2,66 @@ using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text;
|
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() );
|
StringBuilder sb = new(s.Length);
|
||||||
|
foreach( var c in s )
|
||||||
public static string ReplaceInvalidPathSymbols( this string s, string replacement = "_" )
|
|
||||||
{
|
{
|
||||||
StringBuilder sb = new( s.Length );
|
if( Invalid.Contains( c ) )
|
||||||
foreach( var c in s )
|
|
||||||
{
|
{
|
||||||
if( Invalid.Contains( c ) )
|
sb.Append( replacement );
|
||||||
{
|
}
|
||||||
sb.Append( replacement );
|
else
|
||||||
}
|
{
|
||||||
else
|
sb.Append( c );
|
||||||
{
|
|
||||||
sb.Append( c );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return sb.ToString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string RemoveInvalidPathSymbols( this string s )
|
return sb.ToString();
|
||||||
=> string.Concat( s.Split( Path.GetInvalidFileNameChars() ) );
|
}
|
||||||
|
|
||||||
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 );
|
if( c >= 128 )
|
||||||
foreach( var c in s )
|
|
||||||
{
|
{
|
||||||
if( c >= 128 )
|
sb.Append( replacement );
|
||||||
{
|
}
|
||||||
sb.Append( replacement );
|
else
|
||||||
}
|
{
|
||||||
else
|
sb.Append( c );
|
||||||
{
|
|
||||||
sb.Append( c );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return sb.ToString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string ReplaceBadXivSymbols( this string s, string replacement = "_" )
|
return sb.ToString();
|
||||||
{
|
}
|
||||||
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();
|
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.IO;
|
||||||
using System.Linq;
|
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;
|
var name = Path.GetRandomFileName();
|
||||||
for( var i = 0; i < maxTries; ++i )
|
var path = new FileInfo( Path.Combine( baseDir.FullName,
|
||||||
|
suffix.Any() ? name.Substring( 0, name.LastIndexOf( '.' ) ) + suffix : name ) );
|
||||||
|
if( !path.Exists )
|
||||||
{
|
{
|
||||||
var name = Path.GetRandomFileName();
|
return path;
|
||||||
var path = new FileInfo( Path.Combine( baseDir.FullName,
|
|
||||||
suffix.Any() ? name.Substring( 0, name.LastIndexOf( '.' ) ) + suffix : name ) );
|
|
||||||
if( !path.Exists )
|
|
||||||
{
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new IOException();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static FileInfo WriteNew( DirectoryInfo baseDir, byte[] data, string suffix = "" )
|
throw new IOException();
|
||||||
{
|
}
|
||||||
var fileName = TempFileName( baseDir, suffix );
|
|
||||||
using var stream = fileName.OpenWrite();
|
public static FileInfo WriteNew( DirectoryInfo baseDir, byte[] data, string suffix = "" )
|
||||||
stream.Write( data, 0, data.Length );
|
{
|
||||||
fileName.Refresh();
|
var fileName = TempFileName( baseDir, suffix );
|
||||||
return fileName;
|
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