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